From db49641f5b7c7c2184ff99c9337f8857aa9e42b2 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 13 Jun 2025 20:44:39 +0300 Subject: [PATCH 01/35] feat(submit-preprint-state): Implemented state for submit preprint --- .../preprints/store/submit-preprint/index.ts | 4 ++++ .../submit-preprint/submit-preprint.actions.ts | 7 +++++++ .../submit-preprint/submit-preprint.model.ts | 5 +++++ .../submit-preprint.selectors.ts | 10 ++++++++++ .../submit-preprint/submit-preprint.state.ts | 18 ++++++++++++++++++ 5 files changed, 44 insertions(+) create mode 100644 src/app/features/preprints/store/submit-preprint/index.ts create mode 100644 src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts create mode 100644 src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts create mode 100644 src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts create mode 100644 src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts diff --git a/src/app/features/preprints/store/submit-preprint/index.ts b/src/app/features/preprints/store/submit-preprint/index.ts new file mode 100644 index 000000000..cdae9c5e8 --- /dev/null +++ b/src/app/features/preprints/store/submit-preprint/index.ts @@ -0,0 +1,4 @@ +export * from './submit-preprint.actions'; +export * from './submit-preprint.model'; +export * from './submit-preprint.selectors'; +export * from './submit-preprint.state'; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts new file mode 100644 index 000000000..a7235bac2 --- /dev/null +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -0,0 +1,7 @@ +import { StringOrNull } from '@core/helpers'; + +export class SetSelectedPreprintProviderId { + static readonly type = '[Submit Preprint] Set Selected Preprint Provider Id'; + + constructor(public id: StringOrNull) {} +} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts new file mode 100644 index 000000000..4fa5cc4b4 --- /dev/null +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -0,0 +1,5 @@ +import { StringOrNull } from '@core/helpers'; + +export interface SubmitPreprintStateModel { + selectedProviderId: StringOrNull; +} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts new file mode 100644 index 000000000..ea13a8568 --- /dev/null +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -0,0 +1,10 @@ +import { Selector } from '@ngxs/store'; + +import { SubmitPreprintState, SubmitPreprintStateModel } from '@osf/features/preprints/store/submit-preprint'; + +export class SubmitPreprintSelectors { + @Selector([SubmitPreprintState]) + static getSelectedProviderId(state: SubmitPreprintStateModel) { + return state.selectedProviderId; + } +} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts new file mode 100644 index 000000000..3c89163f4 --- /dev/null +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -0,0 +1,18 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { Injectable } from '@angular/core'; + +import { SetSelectedPreprintProviderId, SubmitPreprintStateModel } from './'; + +@State({ + name: 'submitPreprint', +}) +@Injectable() +export class SubmitPreprintState { + @Action(SetSelectedPreprintProviderId) + setSelectedPreprintProviderId(ctx: StateContext, action: SetSelectedPreprintProviderId) { + ctx.patchState({ + selectedProviderId: action.id, + }); + } +} From 14acced7b74190f8d896fdd8d38cc204df8ab9ed Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 13 Jun 2025 20:47:31 +0300 Subject: [PATCH 02/35] feat(select-preprint-state): Implemented select service page with api integration --- .../preprint-services.component.ts | 4 +- .../preprints/constants/preprints.routes.ts | 9 ++++ .../preprints/mappers/preprints.mapper.ts | 20 +++---- .../preprints/models/preprints.models.ts | 5 +- .../landing/preprints-landing.component.html | 1 + .../select-preprint-service.component.html | 54 +++++++++++++++++++ .../select-preprint-service.component.scss | 15 ++++++ .../select-preprint-service.component.spec.ts | 22 ++++++++ .../select-preprint-service.component.ts | 53 ++++++++++++++++++ .../preprints/services/preprints.service.ts | 20 +++++-- .../store/preprints/preprints.actions.ts | 4 ++ .../store/preprints/preprints.model.ts | 5 +- .../store/preprints/preprints.selectors.ts | 10 ++++ .../store/preprints/preprints.state.ts | 25 +++++++++ 14 files changed, 229 insertions(+), 18 deletions(-) create mode 100644 src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html create mode 100644 src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss create mode 100644 src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts create mode 100644 src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.ts b/src/app/features/preprints/components/preprint-services/preprint-services.component.ts index 565d7f54a..5e7b99120 100644 --- a/src/app/features/preprints/components/preprint-services/preprint-services.component.ts +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.ts @@ -3,7 +3,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { PreprintProviderToAdvertise } from '@osf/features/preprints/models'; +import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; @Component({ selector: 'osf-preprint-services', @@ -13,5 +13,5 @@ import { PreprintProviderToAdvertise } from '@osf/features/preprints/models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PreprintServicesComponent { - preprintProvidersToAdvertise = input.required(); + preprintProvidersToAdvertise = input.required(); } diff --git a/src/app/features/preprints/constants/preprints.routes.ts b/src/app/features/preprints/constants/preprints.routes.ts index bdca37437..4832d5795 100644 --- a/src/app/features/preprints/constants/preprints.routes.ts +++ b/src/app/features/preprints/constants/preprints.routes.ts @@ -7,6 +7,7 @@ import { PreprintsState } from '@osf/features/preprints/store/preprints'; import { PreprintsDiscoverState } from '@osf/features/preprints/store/preprints-discover'; import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/preprints-resources-filters'; import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { SubmitPreprintState } from '@osf/features/preprints/store/submit-preprint'; export const preprintsRoutes: Routes = [ { @@ -18,6 +19,7 @@ export const preprintsRoutes: Routes = [ PreprintsDiscoverState, PreprintsResourcesFiltersState, PreprintsResourcesFiltersOptionsState, + SubmitPreprintState, ]), ], children: [ @@ -47,6 +49,13 @@ export const preprintsRoutes: Routes = [ (c) => c.PreprintProviderDiscoverComponent ), }, + { + path: 'select', + loadComponent: () => + import('@osf/features/preprints/pages/select-preprint-service/select-preprint-service.component').then( + (c) => c.SelectPreprintServiceComponent + ), + }, ], }, ]; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 5370489cb..50ac284c5 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -1,7 +1,7 @@ import { PreprintProviderDetails, PreprintProviderDetailsGetResponse, - PreprintProviderToAdvertise, + PreprintProviderShortInfo, Subject, SubjectGetResponse, } from '@osf/features/preprints/models'; @@ -32,16 +32,16 @@ export class PreprintsMapper { }; } - static fromPreprintProvidersToAdvertiseGetResponse( + static toPreprintProviderShortInfoFromGetResponse( response: PreprintProviderDetailsGetResponse[] - ): PreprintProviderToAdvertise[] { - return response - .filter((item) => !item.id.includes('osf')) - .map((item) => ({ - id: item.id, - name: item.attributes.name, - whiteWideImageUrl: item.attributes.assets.wide_white, - })); + ): PreprintProviderShortInfo[] { + return response.map((item) => ({ + id: item.id, + descriptionHtml: item.attributes.description, + name: item.attributes.name, + whiteWideImageUrl: item.attributes.assets?.wide_white, + squareColorNoTransparentImageUrl: item.attributes.assets?.square_color_no_transparent, + })); } static fromSubjectsGetResponse(providerId: string, response: SubjectGetResponse[]): Subject[] { diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 3c6fba4a6..031988711 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -26,10 +26,12 @@ export interface PreprintProviderDetails { iri: string; } -export interface PreprintProviderToAdvertise { +export interface PreprintProviderShortInfo { id: string; name: string; + descriptionHtml: string; whiteWideImageUrl: string; + squareColorNoTransparentImageUrl: string; } export interface Subject { @@ -53,6 +55,7 @@ export interface PreprintProviderDetailsGetResponse { preprint_word: string; assets: { wide_white: string; + square_color_no_transparent: string; }; allow_submissions: boolean; }; diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.html b/src/app/features/preprints/pages/landing/preprints-landing.component.html index 5021901d2..d20f27ed9 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.html +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.html @@ -11,6 +11,7 @@

{{ 'preprints.title' | translate }}

class="ml-auto w-full md:w-auto" styleClass="w-full md:w-auto" [label]="'preprints.addPreprint' | translate: { preprintWord: osfPreprintProvider()?.preprintWord }" + routerLink="/preprints/select" (click)="addPreprint()" /> } diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html new file mode 100644 index 000000000..c363dd66a --- /dev/null +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html @@ -0,0 +1,54 @@ + + +
+

Select a preprint service

+

+ + A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal + peer review and published in a scientific journal. + + Learn more +

+
+ @if (areProvidersLoading()) { + @for (_ of skeletonArray; track $index) { + +
+ + + +
+
+ } + } @else { + @for (provider of preprintProvidersAllowingSubmissions(); track $index) { + +
+ @if (provider.squareColorNoTransparentImageUrl) { + Provider Logo + } + +

{{ provider.name }}

+ +
+
+ +
+
+ } + } +
+
+ +
+
diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss new file mode 100644 index 000000000..51241f5b3 --- /dev/null +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss @@ -0,0 +1,15 @@ +@use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; + +.grid-item { + width: mix.rem(260px); + height: mix.rem(200px); +} + +.active, +p-card:hover { + --p-card-background: var(--bg-blue-3); + cursor: pointer; + + --p-button-secondary-background: var(--white); +} diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts new file mode 100644 index 000000000..d78af628a --- /dev/null +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectPreprintServiceComponent } from './select-preprint-service.component'; + +describe('SelectPreprintServiceComponent', () => { + let component: SelectPreprintServiceComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectPreprintServiceComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectPreprintServiceComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts new file mode 100644 index 000000000..13b8302a7 --- /dev/null +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts @@ -0,0 +1,53 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { Skeleton } from 'primeng/skeleton'; +import { Tooltip } from 'primeng/tooltip'; + +import { NgClass } from '@angular/common'; +import { ChangeDetectionStrategy, Component, HostBinding, OnInit } from '@angular/core'; + +import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; +import { GetPreprintProvidersAllowingSubmissions, PreprintsSelectors } from '@osf/features/preprints/store/preprints'; +import { SetSelectedPreprintProviderId, SubmitPreprintSelectors } from '@osf/features/preprints/store/submit-preprint'; +import { SubHeaderComponent } from '@shared/components'; +import { DecodeHtmlPipe } from '@shared/pipes'; + +@Component({ + selector: 'osf-select-preprint-service', + imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton], + templateUrl: './select-preprint-service.component.html', + styleUrl: './select-preprint-service.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SelectPreprintServiceComponent implements OnInit { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; + + private actions = createDispatchMap({ + getPreprintProvidersAllowingSubmissions: GetPreprintProvidersAllowingSubmissions, + setSelectedPreprintProviderId: SetSelectedPreprintProviderId, + }); + + preprintProvidersAllowingSubmissions = select(PreprintsSelectors.getPreprintProvidersAllowingSubmissions); + areProvidersLoading = select(PreprintsSelectors.arePreprintProvidersAllowingSubmissionsLoading); + selectedProviderId = select(SubmitPreprintSelectors.getSelectedProviderId); + skeletonArray = Array.from({ length: 8 }, (_, i) => i + 1); + + ngOnInit(): void { + this.actions.getPreprintProvidersAllowingSubmissions(); + } + + selectDeselectProvider(provider: PreprintProviderShortInfo) { + if (provider.id === this.selectedProviderId()) { + this.actions.setSelectedPreprintProviderId(null); + return; + } + + this.actions.setSelectedPreprintProviderId(provider.id); + } + + nextStep() { + //[RNi] TODO: redirect to first step of submitting preprint + } +} diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index d83cce72b..9d5981aa8 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -8,7 +8,7 @@ import { PreprintsMapper } from '@osf/features/preprints/mappers'; import { PreprintProviderDetails, PreprintProviderDetailsGetResponse, - PreprintProviderToAdvertise, + PreprintProviderShortInfo, Subject, SubjectGetResponse, } from '@osf/features/preprints/models'; @@ -32,14 +32,28 @@ export class PreprintsService { ); } - getPreprintProvidersToAdvertise(): Observable { + getPreprintProvidersToAdvertise(): Observable { return this.jsonApiService .get< JsonApiResponse >(`${this.baseUrl}?filter[advertise_on_discover_page]=true&reload=true`) .pipe( map((response) => { - return PreprintsMapper.fromPreprintProvidersToAdvertiseGetResponse(response.data); + return PreprintsMapper.toPreprintProviderShortInfoFromGetResponse( + response.data.filter((item) => !item.id.includes('osf')) + ); + }) + ); + } + + getPreprintProvidersAllowingSubmissions(): Observable { + return this.jsonApiService + .get< + JsonApiResponse + >(`${this.baseUrl}?filter[allow_submissions]=true`) + .pipe( + map((response) => { + return PreprintsMapper.toPreprintProviderShortInfoFromGetResponse(response.data); }) ); } diff --git a/src/app/features/preprints/store/preprints/preprints.actions.ts b/src/app/features/preprints/store/preprints/preprints.actions.ts index 5fc7c95fb..9254138a9 100644 --- a/src/app/features/preprints/store/preprints/preprints.actions.ts +++ b/src/app/features/preprints/store/preprints/preprints.actions.ts @@ -13,3 +13,7 @@ export class GetHighlightedSubjectsByProviderId { export class GetPreprintProvidersToAdvertise { static readonly type = '[Preprints] Get Preprint Providers To Advertise'; } + +export class GetPreprintProvidersAllowingSubmissions { + static readonly type = '[Preprints] Get Preprint Providers That Allows Submissions'; +} diff --git a/src/app/features/preprints/store/preprints/preprints.model.ts b/src/app/features/preprints/store/preprints/preprints.model.ts index 4a39aca96..d9bc5680d 100644 --- a/src/app/features/preprints/store/preprints/preprints.model.ts +++ b/src/app/features/preprints/store/preprints/preprints.model.ts @@ -1,8 +1,9 @@ -import { PreprintProviderDetails, PreprintProviderToAdvertise, Subject } from '@osf/features/preprints/models'; +import { PreprintProviderDetails, PreprintProviderShortInfo, Subject } from '@osf/features/preprints/models'; import { AsyncStateModel } from '@shared/models'; export interface PreprintsStateModel { preprintProvidersDetails: AsyncStateModel; - preprintProvidersToAdvertise: AsyncStateModel; + preprintProvidersToAdvertise: AsyncStateModel; + preprintProvidersAllowingSubmissions: AsyncStateModel; highlightedSubjectsForProvider: AsyncStateModel; } diff --git a/src/app/features/preprints/store/preprints/preprints.selectors.ts b/src/app/features/preprints/store/preprints/preprints.selectors.ts index 29f3e15a0..7c3ac8de3 100644 --- a/src/app/features/preprints/store/preprints/preprints.selectors.ts +++ b/src/app/features/preprints/store/preprints/preprints.selectors.ts @@ -19,6 +19,16 @@ export class PreprintsSelectors { return state.preprintProvidersToAdvertise.data; } + @Selector([PreprintsState]) + static getPreprintProvidersAllowingSubmissions(state: PreprintsStateModel) { + return state.preprintProvidersAllowingSubmissions.data; + } + + @Selector([PreprintsState]) + static arePreprintProvidersAllowingSubmissionsLoading(state: PreprintsStateModel) { + return state.preprintProvidersAllowingSubmissions.isLoading; + } + @Selector([PreprintsState]) static getHighlightedSubjectsForProvider(state: PreprintsStateModel) { return state.highlightedSubjectsForProvider.data; diff --git a/src/app/features/preprints/store/preprints/preprints.state.ts b/src/app/features/preprints/store/preprints/preprints.state.ts index 45cc5dba1..d7a0b237a 100644 --- a/src/app/features/preprints/store/preprints/preprints.state.ts +++ b/src/app/features/preprints/store/preprints/preprints.state.ts @@ -10,6 +10,7 @@ import { PreprintsService } from '@osf/features/preprints/services'; import { GetHighlightedSubjectsByProviderId, GetPreprintProviderById, + GetPreprintProvidersAllowingSubmissions, GetPreprintProvidersToAdvertise, } from '@osf/features/preprints/store/preprints/preprints.actions'; @@ -28,6 +29,11 @@ import { PreprintsStateModel } from './'; isLoading: false, error: null, }, + preprintProvidersAllowingSubmissions: { + data: [], + isLoading: false, + error: null, + }, highlightedSubjectsForProvider: { data: [], isLoading: false, @@ -91,6 +97,25 @@ export class PreprintsState { ); } + @Action(GetPreprintProvidersAllowingSubmissions) + getPreprintProvidersAllowingSubmissions(ctx: StateContext) { + ctx.setState(patch({ preprintProvidersAllowingSubmissions: patch({ isLoading: true }) })); + + return this.#preprintsService.getPreprintProvidersAllowingSubmissions().pipe( + tap((data) => { + ctx.setState( + patch({ + preprintProvidersAllowingSubmissions: patch({ + data: data, + isLoading: false, + }), + }) + ); + }), + catchError((error) => this.handleError(ctx, 'preprintProvidersAllowingSubmissions', error)) + ); + } + @Action(GetHighlightedSubjectsByProviderId) getHighlightedSubjectsByProviderId( ctx: StateContext, From 6d65b1bc78e7000f2138b9917acfc45323a6d331 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 13 Jun 2025 22:55:05 +0300 Subject: [PATCH 03/35] feat(select-preprint-service): Enhance UI with dynamic translations --- .../preprint-provider-hero.component.html | 6 +++-- .../preprint-provider-hero.component.ts | 13 +++++++++-- .../landing/preprints-landing.component.html | 4 ++-- .../landing/preprints-landing.component.ts | 2 ++ .../select-preprint-service.component.html | 23 +++++++++++++------ .../select-preprint-service.component.ts | 4 +++- src/assets/i18n/en.json | 13 ++++++++++- 7 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html index 117f77e01..9b3122b6c 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html @@ -21,7 +21,7 @@

{{ preprintProvider()!.name }}

} @@ -56,7 +56,9 @@

{{ preprintProvider()!.name }}

diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts index 034e63039..3d9c89417 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts @@ -4,7 +4,7 @@ import { Button } from 'primeng/button'; import { DialogService } from 'primeng/dynamicdialog'; import { Skeleton } from 'primeng/skeleton'; -import { NgOptimizedImage } from '@angular/common'; +import { NgOptimizedImage, TitleCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; import { FormControl } from '@angular/forms'; import { RouterLink } from '@angular/router'; @@ -16,7 +16,16 @@ import { DecodeHtmlPipe } from '@shared/pipes'; @Component({ selector: 'osf-preprint-provider-hero', - imports: [Button, RouterLink, SearchInputComponent, Skeleton, TranslatePipe, NgOptimizedImage, DecodeHtmlPipe], + imports: [ + Button, + RouterLink, + SearchInputComponent, + Skeleton, + TranslatePipe, + NgOptimizedImage, + DecodeHtmlPipe, + TitleCasePipe, + ], templateUrl: './preprint-provider-hero.component.html', styleUrl: './preprint-provider-hero.component.scss', providers: [DialogService], diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.html b/src/app/features/preprints/pages/landing/preprints-landing.component.html index d20f27ed9..4bbdae484 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.html +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.html @@ -10,7 +10,7 @@

{{ 'preprints.title' | translate }}

@@ -32,7 +32,7 @@

{{ 'preprints.title' | translate }}

diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.ts index 4eaa63df4..ff1f0160b 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.ts +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; +import { TitleCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { Router, RouterLink } from '@angular/router'; @@ -35,6 +36,7 @@ import { ResourceTab } from '@shared/enums'; BrowseBySubjectsComponent, Skeleton, TranslateModule, + TitleCasePipe, ], templateUrl: './preprints-landing.component.html', styleUrl: './preprints-landing.component.scss', diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html index c363dd66a..7152984fc 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html @@ -1,13 +1,14 @@ - +
-

Select a preprint service

+

{{ 'preprints.selectService.sectionTitle' | translate }}

- A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal - peer review and published in a scientific journal. + {{ 'preprints.selectService.description' | translate }} - Learn more + {{ + 'preprints.selectService.learnMore' | translate + }}

@if (areProvidersLoading()) { @@ -37,7 +38,11 @@

Select a preprint service

{{ provider.name }}

@@ -49,6 +54,10 @@

{{ provider.name }}

}
- +
diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts index 13b8302a7..e8cadaed8 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts @@ -1,5 +1,7 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { TranslateModule } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { Skeleton } from 'primeng/skeleton'; @@ -16,7 +18,7 @@ import { DecodeHtmlPipe } from '@shared/pipes'; @Component({ selector: 'osf-select-preprint-service', - imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton], + imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton, TranslateModule], templateUrl: './select-preprint-service.component.html', styleUrl: './select-preprint-service.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 72faf4dba..31dc2ffcb 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1229,7 +1229,7 @@ "title": "Preprints", "addPreprint": "Add a {{preprintWord}}", "poweredBy": "Powered by OSF Preprints", - "searchPlaceholder": "Search Preprints", + "searchPlaceholder": "Search {{preprintWord}}s", "showExample": "Show an example", "browseBySubjects": { "title": "Browse By Subjects" @@ -1251,6 +1251,17 @@ "header": "Search help", "message": "OSF Search provides a powerful discovery tool to help you find data, papers, analysis plans, and more content across the research lifecycle. OSF Preprints has some specialized filters, which you can learn more about on our", "linkText": "help guides" + }, + "selectService": { + "title": "Add A Preprint", + "sectionTitle": "Select a preprint service", + "description": "A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal peer review and published in a scientific journal.", + "learnMore": "Learn more", + "buttons": { + "next": "Next", + "select": "Select", + "deselect": "Deselect" + } } }, "truncatedText": { From 4efc2984d2c2185f8eb7331c429ea11c9ef49693 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 13 Jun 2025 22:55:05 +0300 Subject: [PATCH 04/35] feat(select-preprint-service): Enhance UI with dynamic translations --- .../preprint-provider-hero.component.html | 6 +++-- .../preprint-provider-hero.component.ts | 13 +++++++++-- .../landing/preprints-landing.component.html | 5 ++-- .../landing/preprints-landing.component.ts | 6 ++--- .../select-preprint-service.component.html | 23 +++++++++++++------ .../select-preprint-service.component.ts | 4 +++- src/assets/i18n/en.json | 13 ++++++++++- 7 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html index 117f77e01..9b3122b6c 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html @@ -21,7 +21,7 @@

{{ preprintProvider()!.name }}

} @@ -56,7 +56,9 @@

{{ preprintProvider()!.name }}

diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts index 034e63039..3d9c89417 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts @@ -4,7 +4,7 @@ import { Button } from 'primeng/button'; import { DialogService } from 'primeng/dynamicdialog'; import { Skeleton } from 'primeng/skeleton'; -import { NgOptimizedImage } from '@angular/common'; +import { NgOptimizedImage, TitleCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; import { FormControl } from '@angular/forms'; import { RouterLink } from '@angular/router'; @@ -16,7 +16,16 @@ import { DecodeHtmlPipe } from '@shared/pipes'; @Component({ selector: 'osf-preprint-provider-hero', - imports: [Button, RouterLink, SearchInputComponent, Skeleton, TranslatePipe, NgOptimizedImage, DecodeHtmlPipe], + imports: [ + Button, + RouterLink, + SearchInputComponent, + Skeleton, + TranslatePipe, + NgOptimizedImage, + DecodeHtmlPipe, + TitleCasePipe, + ], templateUrl: './preprint-provider-hero.component.html', styleUrl: './preprint-provider-hero.component.scss', providers: [DialogService], diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.html b/src/app/features/preprints/pages/landing/preprints-landing.component.html index d20f27ed9..31804ca5c 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.html +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.html @@ -10,9 +10,8 @@

{{ 'preprints.title' | translate }}

} @@ -32,7 +31,7 @@

{{ 'preprints.title' | translate }}

diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.ts index 4eaa63df4..808e62662 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.ts +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; +import { TitleCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, effect, HostBinding, inject, OnDestroy, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { Router, RouterLink } from '@angular/router'; @@ -35,6 +36,7 @@ import { ResourceTab } from '@shared/enums'; BrowseBySubjectsComponent, Skeleton, TranslateModule, + TitleCasePipe, ], templateUrl: './preprints-landing.component.html', styleUrl: './preprints-landing.component.scss', @@ -59,10 +61,6 @@ export class PreprintsLandingComponent implements OnInit, OnDestroy { highlightedSubjectsByProviderId = select(PreprintsSelectors.getHighlightedSubjectsForProvider); areSubjectsLoading = select(PreprintsSelectors.areSubjectsLoading); - addPreprint() { - // [RNi] TODO: Implement the logic to add a preprint. - } - constructor() { effect(() => { const provider = this.osfPreprintProvider(); diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html index c363dd66a..7152984fc 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html @@ -1,13 +1,14 @@ - +
-

Select a preprint service

+

{{ 'preprints.selectService.sectionTitle' | translate }}

- A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal - peer review and published in a scientific journal. + {{ 'preprints.selectService.description' | translate }} - Learn more + {{ + 'preprints.selectService.learnMore' | translate + }}

@if (areProvidersLoading()) { @@ -37,7 +38,11 @@

Select a preprint service

{{ provider.name }}

@@ -49,6 +54,10 @@

{{ provider.name }}

}
- +
diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts index 13b8302a7..e8cadaed8 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts @@ -1,5 +1,7 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { TranslateModule } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { Skeleton } from 'primeng/skeleton'; @@ -16,7 +18,7 @@ import { DecodeHtmlPipe } from '@shared/pipes'; @Component({ selector: 'osf-select-preprint-service', - imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton], + imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton, TranslateModule], templateUrl: './select-preprint-service.component.html', styleUrl: './select-preprint-service.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 72faf4dba..31dc2ffcb 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1229,7 +1229,7 @@ "title": "Preprints", "addPreprint": "Add a {{preprintWord}}", "poweredBy": "Powered by OSF Preprints", - "searchPlaceholder": "Search Preprints", + "searchPlaceholder": "Search {{preprintWord}}s", "showExample": "Show an example", "browseBySubjects": { "title": "Browse By Subjects" @@ -1251,6 +1251,17 @@ "header": "Search help", "message": "OSF Search provides a powerful discovery tool to help you find data, papers, analysis plans, and more content across the research lifecycle. OSF Preprints has some specialized filters, which you can learn more about on our", "linkText": "help guides" + }, + "selectService": { + "title": "Add A Preprint", + "sectionTitle": "Select a preprint service", + "description": "A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal peer review and published in a scientific journal.", + "learnMore": "Learn more", + "buttons": { + "next": "Next", + "select": "Select", + "deselect": "Deselect" + } } }, "truncatedText": { From 5bb72fb20ebe426e046419187deaebf87d344b61 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 11:44:23 +0300 Subject: [PATCH 05/35] fix(preprint-provider-hero): Added skeleton for search input while preprint provider is loading --- .../preprint-provider-hero.component.html | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html index 9b3122b6c..5c03e97bc 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html @@ -41,33 +41,39 @@

{{ preprintProvider()!.name }}

} -
- preprints help + @if (isPreprintProviderLoading()) { + + } @else { +
+ preprints help - -
+ +
+ } @if (isPreprintProviderLoading()) { } @else { - {{ 'preprints.showExample' | translate }} +

+ {{ 'preprints.showExample' | translate }} +

} From 0a358c769031b891d43e5dfaa9241153544abe12 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 11:51:17 +0300 Subject: [PATCH 06/35] fix(preprints-styles): Fixed styles due to merge conflicts --- .../pages/landing/preprints-landing.component.html | 2 +- .../pages/landing/preprints-landing.component.scss | 8 ++++++++ src/assets/styles/components/preprints.scss | 7 +++++++ src/assets/styles/overrides/select.scss | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.html b/src/app/features/preprints/pages/landing/preprints-landing.component.html index 31804ca5c..ab5d2de10 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.html +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.html @@ -76,7 +76,7 @@

{{ 'preprints.createServer.title' | translate }}

- + {{ 'preprints.createServer.contactUs' | translate }} diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.scss b/src/app/features/preprints/pages/landing/preprints-landing.component.scss index 59c5ab2a5..8cae1f8ce 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.scss +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.scss @@ -12,3 +12,11 @@ .blue-dark-gradient { background: url("/assets/images/dark-blue-gradient.png") center; } + +.link-button { + a, + a:hover { + color: var(--p-button-success-color); + text-decoration: none; + } +} diff --git a/src/assets/styles/components/preprints.scss b/src/assets/styles/components/preprints.scss index db2b59aca..88fae462e 100644 --- a/src/assets/styles/components/preprints.scss +++ b/src/assets/styles/components/preprints.scss @@ -15,6 +15,12 @@ line-height: mix.rem(24px); } + .search-container { + .search-input:hover { + border-color: var(--preprints-branding-primary-color); + } + } + a { color: var(--preprints-branding-primary-color); } @@ -23,6 +29,7 @@ .p-button { color: var(--preprints-branding-secondary-color); background-color: var(--preprints-branding-primary-color); + border-color: var(--preprints-branding-secondary-color); } } } diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index b09629282..f1b3d37ef 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -105,7 +105,7 @@ .filter { .p-select { .p-select-label { - font-size: 1.2rem; + font-size: mix.rem(16px); font-weight: 400; color: var.$dark-blue-1; padding-right: 0.5rem; From 7a34b3cfb51b9d927ab95b4a3f6d9890468a620c Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 13:35:26 +0300 Subject: [PATCH 07/35] feat(nav-menu): Added subroutes for preprints --- src/app/core/constants/nav-items.constant.ts | 15 ++++++++++++++- src/assets/i18n/en.json | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 9058c3655..7a86051e0 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -32,7 +32,20 @@ export const NAV_ITEMS: NavItem[] = [ path: '/preprints', label: 'navigation.preprints', icon: 'preprints', - useExactMatch: true, + isCollapsible: true, + useExactMatch: false, + items: [ + { + path: '/preprints', + label: 'navigation.preprintsSubRoutes.overview', + useExactMatch: false, + }, + { + path: '/my-preprints', + label: 'navigation.preprintsSubRoutes.myPreprints', + useExactMatch: true, + }, + ], }, { path: '/my-profile', diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c4bcf237d..5f81fe9e3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -34,6 +34,10 @@ "meetings": "Meetings", "myProjects": "My Projects", "preprints": "Preprints", + "preprintsSubRoutes": { + "overview": "Overview", + "myPreprints": "My Preprints" + }, "collections": "Collections", "moderation": "Moderation", "donate": "Donate", From 3bfc7689f52bef13383fa2568fa2536d50519e56 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 13:37:18 +0300 Subject: [PATCH 08/35] fix(preprints-search-input): Changed placeholder --- src/assets/i18n/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 5f81fe9e3..20f3b44c9 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1210,7 +1210,7 @@ "title": "Preprints", "addPreprint": "Add a {{preprintWord}}", "poweredBy": "Powered by OSF Preprints", - "searchPlaceholder": "Search {{preprintWord}}s", + "searchPlaceholder": "Search {{preprintWord}}...", "showExample": "Show an example", "browseBySubjects": { "title": "Browse By Subjects" From f90c8c794068788b0639e6bb646fc6d6fc5ed591 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 14:00:12 +0300 Subject: [PATCH 09/35] fix(comments): Fixed comments --- .../select-preprint-service.component.html | 19 +++++++++---------- .../select-preprint-service.component.scss | 3 +-- src/assets/i18n/en.json | 10 ++++------ src/assets/styles/overrides/select.scss | 1 - 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html index 7152984fc..c03758447 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html @@ -25,23 +25,26 @@

{{ 'preprints.selectService.sectionTitle' | translate }}

@for (provider of preprintProvidersAllowingSubmissions(); track $index) {
- @if (provider.squareColorNoTransparentImageUrl) { - Provider Logo - } +

{{ provider.name }}

@@ -54,10 +57,6 @@

{{ provider.name }}

}
- +
diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss index 51241f5b3..e4df0f6af 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.scss @@ -1,5 +1,4 @@ @use "assets/styles/mixins" as mix; -@use "assets/styles/variables" as var; .grid-item { width: mix.rem(260px); @@ -7,7 +6,7 @@ } .active, -p-card:hover { +.provider-card:hover { --p-card-background: var(--bg-blue-3); cursor: pointer; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 20f3b44c9..2c637efd5 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -11,7 +11,9 @@ "copy": "Copy", "move": "Move", "rename": "Rename", - "next": "Next" + "next": "Next", + "select": "Select", + "deselect": "Deselect" }, "search": { "title": "Search", @@ -1238,11 +1240,7 @@ "sectionTitle": "Select a preprint service", "description": "A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal peer review and published in a scientific journal.", "learnMore": "Learn more", - "buttons": { - "next": "Next", - "select": "Select", - "deselect": "Deselect" - } + "providerLogoImageAlt": "Provider Logo" } }, "truncatedText": { diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index 68d505df4..61ef05739 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -77,7 +77,6 @@ .p-select { border: none; - font-size: mix.rem(14px); box-shadow: none; height: 2rem; From 964f3108c687a4b7c44140ebeec3306dc58dcd8c Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 14:34:09 +0300 Subject: [PATCH 10/35] fix(preprints-help-dialog): Fixed url to help guide --- .../preprints-help-dialog/preprints-help-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.html b/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.html index e19556eda..ef1b931d3 100644 --- a/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.html +++ b/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.html @@ -1,7 +1,7 @@

{{ 'preprints.helpDialog.message' | translate }} - {{ 'preprints.helpDialog.linkText' | translate }}.

From 8aef89993a39c7fcafef489b33c5dc47b7ed438a Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 18:05:23 +0300 Subject: [PATCH 11/35] feat(preprints-favicon): Implemented updating browser tab favicon and title with preprint branding --- .../preprints/mappers/preprints.mapper.ts | 1 + .../preprints/models/preprints.models.ts | 2 ++ .../preprint-provider-discover.component.ts | 4 +++- .../preprint-provider-overview.component.ts | 4 +++- src/app/shared/utils/browser-tab.helper.ts | 22 +++++++++++++++++++ src/app/shared/utils/index.ts | 1 + 6 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/utils/browser-tab.helper.ts diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 50ac284c5..aff80c2ee 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -29,6 +29,7 @@ export class PreprintsMapper { secondaryColor: brandRaw.attributes.secondary_color, }, iri: response.links.iri, + faviconUrl: response.attributes.assets.favicon, }; } diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 031988711..26280982c 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -24,6 +24,7 @@ export interface PreprintProviderDetails { brand: Brand; lastFetched?: number; iri: string; + faviconUrl: string; } export interface PreprintProviderShortInfo { @@ -56,6 +57,7 @@ export interface PreprintProviderDetailsGetResponse { assets: { wide_white: string; square_color_no_transparent: string; + favicon: string; }; allow_submissions: boolean; }; diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts index 41689ad64..b9d5b1a70 100644 --- a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts @@ -41,7 +41,7 @@ import { } from '@osf/features/preprints/store/preprints-resources-filters'; import { GetAllOptions } from '@osf/features/preprints/store/preprints-resources-filters-options'; import { FilterLabelsModel, ResourceFilterLabel } from '@shared/models'; -import { HeaderStyleHelper } from '@shared/utils'; +import { BrowserTabHelper, HeaderStyleHelper } from '@shared/utils'; @Component({ selector: 'osf-preprint-provider-discover', @@ -110,6 +110,7 @@ export class PreprintProviderDiscoverComponent implements OnInit, OnDestroy { provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); + BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }); @@ -187,6 +188,7 @@ export class PreprintProviderDiscoverComponent implements OnInit, OnDestroy { ngOnDestroy() { HeaderStyleHelper.resetToDefaults(); BrandService.resetBranding(); + BrowserTabHelper.resetToDefaults(); this.actions.resetFiltersState(); this.actions.resetDiscoverState(); } diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts index ade20c7e1..a00d94d17 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts @@ -15,7 +15,7 @@ import { GetPreprintProviderById, PreprintsSelectors, } from '@osf/features/preprints/store/preprints'; -import { HeaderStyleHelper } from '@shared/utils'; +import { BrowserTabHelper, HeaderStyleHelper } from '@shared/utils'; @Component({ selector: 'osf-provider-overview', @@ -55,6 +55,7 @@ export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); + BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }); } @@ -67,6 +68,7 @@ export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { ngOnDestroy() { HeaderStyleHelper.resetToDefaults(); BrandService.resetBranding(); + BrowserTabHelper.resetToDefaults(); } redirectToDiscoverPageWithValue(searchValue: string) { diff --git a/src/app/shared/utils/browser-tab.helper.ts b/src/app/shared/utils/browser-tab.helper.ts new file mode 100644 index 000000000..f0df8afef --- /dev/null +++ b/src/app/shared/utils/browser-tab.helper.ts @@ -0,0 +1,22 @@ +export class BrowserTabHelper { + private static readonly DEFAULT_FAVICON = '/favicon.ico'; + private static readonly DEFAULT_TITLE = 'OSF'; + + static updateTabStyles(faviconUrl: string, title: string) { + if (faviconUrl) { + const faviconElement = document.querySelector("link[rel*='icon']") as HTMLLinkElement; + faviconElement.href = faviconUrl; + } + + if (title) { + document.title = title; + } + } + + static resetToDefaults() { + const faviconElement = document.querySelector("link[rel*='icon']") as HTMLLinkElement; + faviconElement.href = this.DEFAULT_FAVICON; + + document.title = this.DEFAULT_TITLE; + } +} diff --git a/src/app/shared/utils/index.ts b/src/app/shared/utils/index.ts index ec1ea68b3..38d45729d 100644 --- a/src/app/shared/utils/index.ts +++ b/src/app/shared/utils/index.ts @@ -1,6 +1,7 @@ export { HeaderStyleHelper } from '../utils/header-style.helper'; export * from './add-filters-params.helper'; export * from './breakpoints.tokens'; +export { BrowserTabHelper } from './browser-tab.helper'; export * from './custom-form-validators.helper'; export * from './default-confirmation-config.helper'; export * from './find-changed-items.helper'; From dffc84c555bc6570b14efd545d6f32c2c4b897e7 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 16 Jun 2025 18:26:09 +0300 Subject: [PATCH 12/35] fix(select): remove duplicate style --- src/assets/styles/overrides/select.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index 61ef05739..144800069 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -108,7 +108,6 @@ .filter { .p-select { .p-select-label { - font-size: mix.rem(16px); font-weight: 400; color: var.$dark-blue-1; padding-right: 0.5rem; From 2c7a628c1e4dced1adfb09fa4531f104f901a69a Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 17 Jun 2025 15:02:36 +0300 Subject: [PATCH 13/35] feat(stepper): Implemented shared component --- src/app/shared/components/index.ts | 1 + .../components/stepper/stepper.component.html | 21 +++++++ .../components/stepper/stepper.component.scss | 61 +++++++++++++++++++ .../stepper/stepper.component.spec.ts | 22 +++++++ .../components/stepper/stepper.component.ts | 31 ++++++++++ src/app/shared/models/index.ts | 1 + src/app/shared/models/stepper-step.model.ts | 4 ++ src/assets/styles/_variables.scss | 9 +++ 8 files changed, 150 insertions(+) create mode 100644 src/app/shared/components/stepper/stepper.component.html create mode 100644 src/app/shared/components/stepper/stepper.component.scss create mode 100644 src/app/shared/components/stepper/stepper.component.spec.ts create mode 100644 src/app/shared/components/stepper/stepper.component.ts create mode 100644 src/app/shared/models/stepper-step.model.ts diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index b4f37c2e0..265ee73fa 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -18,6 +18,7 @@ export { ResourceCardComponent } from './resource-card/resource-card.component'; export { SearchHelpTutorialComponent } from './search-help-tutorial/search-help-tutorial.component'; export { SearchInputComponent } from './search-input/search-input.component'; export { SelectComponent } from './select/select.component'; +export { StepperComponent } from './stepper/stepper.component'; export { SubHeaderComponent } from './sub-header/sub-header.component'; export { TextInputComponent } from './text-input/text-input.component'; export { ToastComponent } from './toast/toast.component'; diff --git a/src/app/shared/components/stepper/stepper.component.html b/src/app/shared/components/stepper/stepper.component.html new file mode 100644 index 000000000..b647786b1 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.component.html @@ -0,0 +1,21 @@ +
+
+ @for (step of steps(); track step.value; let i = $index) { + + @if (i < steps().length - 1) { +
+ } + } +
+
+ @for (step of steps(); track step.value) { +
{{ step.label | translate }}
+ } +
+
diff --git a/src/app/shared/components/stepper/stepper.component.scss b/src/app/shared/components/stepper/stepper.component.scss new file mode 100644 index 000000000..9fbf90628 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.component.scss @@ -0,0 +1,61 @@ +@use "assets/styles/mixins" as mix; + +.stepper-container { + display: flex; + flex-direction: column; + align-items: start; + width: fit-content; + + .stepper-circles-lines-row { + display: flex; + align-items: center; + padding: 0 mix.rem(24px); + + .step-circle { + width: mix.rem(32px); + height: mix.rem(32px); + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: var(--stepper-step-color); + background: var(--stepper-step-background); + border: 1px solid var(--stepper-step-border-color); + font-size: mix.rem(16px); + cursor: pointer; + + &.active { + background: var(--stepper-active-step-background); + color: var(--stepper-active-step-color); + border: 1px solid var(--stepper-active-step-border-color); + } + } + + .step-line { + width: mix.rem(80px); + height: mix.rem(2px); + margin: 0 mix.rem(8px); + background: var(--stepper-space-line-color); + + &.active { + background: var(--stepper-active-space-line-color); + } + } + } + + .stepper-labels-row { + display: flex; + align-items: start; + margin-top: mix.rem(10px); + + .step-label { + width: 80px; + text-align: center; + margin-right: 48px; + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/src/app/shared/components/stepper/stepper.component.spec.ts b/src/app/shared/components/stepper/stepper.component.spec.ts new file mode 100644 index 000000000..a52a27c62 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StepperComponent } from '@shared/components'; + +describe('StepperComponent', () => { + let component: StepperComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StepperComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(StepperComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/stepper/stepper.component.ts b/src/app/shared/components/stepper/stepper.component.ts new file mode 100644 index 000000000..79dd0f825 --- /dev/null +++ b/src/app/shared/components/stepper/stepper.component.ts @@ -0,0 +1,31 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, input, model } from '@angular/core'; + +import { IconComponent } from '@shared/components'; +import { StepperStep } from '@shared/models'; + +@Component({ + selector: 'osf-stepper', + imports: [IconComponent, TranslatePipe], + templateUrl: './stepper.component.html', + styleUrl: './stepper.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StepperComponent { + steps = input.required(); + currentStep = model.required(); + linear = input(true); + + onStepClick(step: number) { + if (step === this.currentStep()) { + return; + } + + if (this.linear() && step > this.currentStep()) { + return; + } + + this.currentStep.set(step); + } +} diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 3e8d903bf..3a764cc75 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -12,6 +12,7 @@ export * from './query-params.model'; export * from './resource-card'; export * from './select-option.model'; export * from './social-icon.model'; +export * from './stepper-step.model'; export * from './store'; export * from './tab-option.model'; export * from './table-parameters.model'; diff --git a/src/app/shared/models/stepper-step.model.ts b/src/app/shared/models/stepper-step.model.ts new file mode 100644 index 000000000..9199e35a9 --- /dev/null +++ b/src/app/shared/models/stepper-step.model.ts @@ -0,0 +1,4 @@ +export interface StepperStep { + label: string; + value: number; +} diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss index 2e2f0f38d..296fd6400 100644 --- a/src/assets/styles/_variables.scss +++ b/src/assets/styles/_variables.scss @@ -137,6 +137,15 @@ $white-60: var(--white-60); #f3f8fd; --gradient-3: linear-gradient(90.12deg, #dbedfb 0.03%, #eff4fc 54.38%, #dbedfb 98.07%); + --stepper-step-background: var(--white); + --stepper-active-step-background: var(--pr-blue-1); + --stepper-step-color: var(--dark-blue-1); + --stepper-active-step-color: var(--white); + --stepper-space-line-color: var(--white); + --stepper-active-space-line-color: var(--pr-blue-1); + --stepper-step-border-color: var(--white); + --stepper-active-step-border-color: var(--pr-blue-1); + --header-color: none; --header-background-color: none; --header-background-image-url: none; From 3bc707aff0c61d03ef862fa4ac8264a680e8acd6 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 17 Jun 2025 20:44:12 +0300 Subject: [PATCH 14/35] feat(submit-preprint-stepper): Created parent component for submit component stepper --- .../preprint-provider-hero.component.html | 2 +- .../preprint-provider-hero.component.ts | 5 - src/app/features/preprints/constants/index.ts | 3 + .../preprints/constants/preprints.routes.ts | 7 ++ .../constants/submit-preprint-steps.const.ts | 28 ++++++ .../preprints/models/preprints.models.ts | 43 +++++++++ .../select-preprint-service.component.html | 6 +- .../select-preprint-service.component.ts | 7 +- .../submit-preprint-stepper.component.html | 40 ++++++++ .../submit-preprint-stepper.component.scss | 13 +++ .../submit-preprint-stepper.component.spec.ts | 22 +++++ .../submit-preprint-stepper.component.ts | 93 +++++++++++++++++++ 12 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 src/app/features/preprints/constants/index.ts create mode 100644 src/app/features/preprints/constants/submit-preprint-steps.const.ts create mode 100644 src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html create mode 100644 src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.scss create mode 100644 src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts create mode 100644 src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html index b782dab1e..a6cee4c1a 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html @@ -22,7 +22,7 @@

{{ preprintProvider()!.name }}

class="ml-auto w-full md:w-auto preprints-branding-button" styleClass="w-full md:w-auto" [label]="'preprints.addPreprint' | translate: { preprintWord: preprintProvider()!.preprintWord } | titlecase" - (click)="addPreprint()" + [routerLink]="['/preprints', preprintProvider()!.id, 'submit']" /> }
diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts index 1ace7b354..378a4648a 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts @@ -29,13 +29,8 @@ export class PreprintProviderHeroComponent { searchControl = input(new FormControl()); preprintProvider = input.required(); isPreprintProviderLoading = input.required(); - addPreprintClicked = output(); triggerSearch = output(); - addPreprint() { - this.addPreprintClicked.emit(); - } - onTriggerSearch(value: string) { this.triggerSearch.emit(value); } diff --git a/src/app/features/preprints/constants/index.ts b/src/app/features/preprints/constants/index.ts new file mode 100644 index 000000000..791dc42bc --- /dev/null +++ b/src/app/features/preprints/constants/index.ts @@ -0,0 +1,3 @@ +export * from './form-input-limits.const'; +export * from './preprints.routes'; +export * from './submit-preprint-steps.const'; diff --git a/src/app/features/preprints/constants/preprints.routes.ts b/src/app/features/preprints/constants/preprints.routes.ts index 4832d5795..43fee02e6 100644 --- a/src/app/features/preprints/constants/preprints.routes.ts +++ b/src/app/features/preprints/constants/preprints.routes.ts @@ -56,6 +56,13 @@ export const preprintsRoutes: Routes = [ (c) => c.SelectPreprintServiceComponent ), }, + { + path: ':providerId/submit', + loadComponent: () => + import('@osf/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component').then( + (c) => c.SubmitPreprintStepperComponent + ), + }, ], }, ]; diff --git a/src/app/features/preprints/constants/submit-preprint-steps.const.ts b/src/app/features/preprints/constants/submit-preprint-steps.const.ts new file mode 100644 index 000000000..30acd7751 --- /dev/null +++ b/src/app/features/preprints/constants/submit-preprint-steps.const.ts @@ -0,0 +1,28 @@ +import { StepperStep } from '@shared/models'; + +export const submitPreprintSteps: StepperStep[] = [ + { + label: 'Title and Abstract', + value: 0, + }, + { + label: 'File', + value: 1, + }, + { + label: 'Metadata', + value: 2, + }, + { + label: 'Author Assertions', + value: 3, + }, + { + label: 'Supplements', + value: 4, + }, + { + label: 'Review', + value: 5, + }, +]; diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 26280982c..2032cc511 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -42,6 +42,19 @@ export interface Subject { preprintProviderId: string; } +export interface Preprint { + id: string; + dateCreated: string; + dateModified: string; + title: string; + description: string; + isPublished: boolean; + tags: string[]; + isPublic: boolean; + version: number; + isLatestVersion: boolean; +} + //api models export interface PreprintProviderDetailsGetResponse { id: string; @@ -92,3 +105,33 @@ export interface SubjectGetResponse { taxonomy_name: string; }; } + +export interface PreprintJsonApi { + id: string; + type: 'preprints'; + attributes: { + date_created: string; + date_modified: string; + date_published: Date | null; + original_publication_date: Date | null; + custom_publication_citation: StringOrNull; + doi: StringOrNull; + preprint_doi_created: Date | null; + title: string; + description: string; + is_published: boolean; + is_preprint_orphan: boolean; + license_record: StringOrNull; + tags: string[]; + date_withdrawn: Date | null; + current_user_permissions: string[]; + public: boolean; + reviews_state: string; + date_last_transitioned: Date | null; + version: number; + is_latest_version: boolean; + has_coi: boolean; + conflict_of_interest_statement: StringOrNull; + has_data_links: boolean; + }; +} diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html index c03758447..824f0afe2 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.html @@ -57,6 +57,10 @@

{{ provider.name }}

}
- +
diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts index e8cadaed8..98784e7c7 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.ts @@ -9,6 +9,7 @@ import { Tooltip } from 'primeng/tooltip'; import { NgClass } from '@angular/common'; import { ChangeDetectionStrategy, Component, HostBinding, OnInit } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; import { GetPreprintProvidersAllowingSubmissions, PreprintsSelectors } from '@osf/features/preprints/store/preprints'; @@ -18,7 +19,7 @@ import { DecodeHtmlPipe } from '@shared/pipes'; @Component({ selector: 'osf-select-preprint-service', - imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton, TranslateModule], + imports: [SubHeaderComponent, Card, Button, NgClass, Tooltip, DecodeHtmlPipe, Skeleton, TranslateModule, RouterLink], templateUrl: './select-preprint-service.component.html', styleUrl: './select-preprint-service.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -48,8 +49,4 @@ export class SelectPreprintServiceComponent implements OnInit { this.actions.setSelectedPreprintProviderId(provider.id); } - - nextStep() { - //[RNi] TODO: redirect to first step of submitting preprint - } } 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 new file mode 100644 index 000000000..57a126d1c --- /dev/null +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html @@ -0,0 +1,40 @@ +
+
+ @if (isPreprintProviderLoading()) { + + + } @else { + +

{{ 'Add a ' + preprintProvider()!.preprintWord }}

+ } +
+ + @if (isWeb()) { + @if (isPreprintProviderLoading()) { + + } @else { + + } + } +
+ +
+ @switch (currentStep()) { + @case (0) { + + } + @default { +

No such step

+ } + } +
diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.scss b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.scss new file mode 100644 index 000000000..04a593751 --- /dev/null +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.scss @@ -0,0 +1,13 @@ +.preprints-hero-container { + --stepper-step-background: var(--preprints-branding-secondary-color); + --stepper-active-step-background: var(--preprints-branding-primary-color); + + --stepper-step-color: var(--preprints-branding-primary-color); + --stepper-active-step-color: var(--preprints-branding-secondary-color); + + --stepper-space-line-color: color-mix(in srgb, var(--preprints-branding-primary-color), transparent 75%); + --stepper-active-space-line-color: var(--preprints-branding-primary-color); + + --stepper-step-border-color: color-mix(in srgb, var(--preprints-branding-primary-color), transparent 75%); + --stepper-active-step-border-color: var(--preprints-branding-primary-color); +} diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts new file mode 100644 index 000000000..8f101cd19 --- /dev/null +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmitPreprintStepperComponent } from './submit-preprint-stepper.component'; + +describe('SubmitPreprintStepperComponent', () => { + let component: SubmitPreprintStepperComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubmitPreprintStepperComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmitPreprintStepperComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..ab98788c9 --- /dev/null +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -0,0 +1,93 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { Skeleton } from 'primeng/skeleton'; + +import { map, of } from 'rxjs'; + +import { + ChangeDetectionStrategy, + Component, + effect, + HostBinding, + inject, + OnDestroy, + OnInit, + signal, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute } from '@angular/router'; + +import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; +import { submitPreprintSteps } from '@osf/features/preprints/constants'; +import { BrandService } from '@osf/features/preprints/services'; +import { + GetHighlightedSubjectsByProviderId, + GetPreprintProviderById, + PreprintsSelectors, +} from '@osf/features/preprints/store/preprints'; +import { SetSelectedPreprintProviderId } from '@osf/features/preprints/store/submit-preprint'; +import { StepperComponent } from '@shared/components/stepper/stepper.component'; +import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/utils'; + +@Component({ + selector: 'osf-submit-preprint-stepper', + imports: [Skeleton, StepperComponent, TitleAndAbstractStepComponent], + templateUrl: './submit-preprint-stepper.component.html', + styleUrl: './submit-preprint-stepper.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; + + private readonly route = inject(ActivatedRoute); + + private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])) ?? of(undefined)); + + private actions = createDispatchMap({ + getPreprintProviderById: GetPreprintProviderById, + getHighlightedSubjectsByProviderId: GetHighlightedSubjectsByProviderId, + setSelectedPreprintProviderId: SetSelectedPreprintProviderId, + }); + + readonly submitPreprintSteps = submitPreprintSteps; + + preprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.providerId())); + isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); + currentStep = signal(0); + isWeb = toSignal(inject(IS_WEB)); + + constructor() { + effect(() => { + const provider = this.preprintProvider(); + + if (provider) { + this.actions.setSelectedPreprintProviderId(provider.id); + BrandService.applyBranding(provider.brand); + HeaderStyleHelper.applyHeaderStyles( + provider.brand.primaryColor, + provider.brand.secondaryColor, + provider.brand.heroBackgroundImageUrl + ); + BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); + } + }); + } + + ngOnInit() { + this.actions.getPreprintProviderById(this.providerId()); + } + + ngOnDestroy() { + HeaderStyleHelper.resetToDefaults(); + BrandService.resetBranding(); + BrowserTabHelper.resetToDefaults(); + } + + stepChange(step: number) { + if (step >= this.currentStep()) { + return; + } + + this.currentStep.set(step); + } +} From 48caa83507b2866af272b1d244653dfe9e0bb047 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 17 Jun 2025 20:49:09 +0300 Subject: [PATCH 15/35] feat(submit-preprint-first-step): Partly implemented first step --- .../features/preprints/components/index.ts | 1 + .../title-and-abstract-step.component.html | 41 ++++++++++ .../title-and-abstract-step.component.scss | 0 .../title-and-abstract-step.component.spec.ts | 22 ++++++ .../title-and-abstract-step.component.ts | 76 +++++++++++++++++++ .../constants/form-input-limits.const.ts | 5 ++ .../preprints/mappers/preprints.mapper.ts | 37 +++++++++ src/app/features/preprints/models/index.ts | 1 + .../models/submit-preprint-form.models.ts | 6 ++ .../submit-preprint-stepper.component.ts | 1 + .../preprints/services/preprints.service.ts | 10 +++ .../submit-preprint.actions.ts | 10 +++ .../submit-preprint/submit-preprint.model.ts | 2 + .../submit-preprint.selectors.ts | 5 ++ .../submit-preprint/submit-preprint.state.ts | 25 +++++- 15 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html create mode 100644 src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss create mode 100644 src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.spec.ts create mode 100644 src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts create mode 100644 src/app/features/preprints/constants/form-input-limits.const.ts create mode 100644 src/app/features/preprints/models/submit-preprint-form.models.ts diff --git a/src/app/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts index 0aa788de3..d654f5720 100644 --- a/src/app/features/preprints/components/index.ts +++ b/src/app/features/preprints/components/index.ts @@ -1,5 +1,6 @@ export { BrowseBySubjectsComponent } from './browse-by-subjects/browse-by-subjects.component'; export { PreprintServicesComponent } from './preprint-services/preprint-services.component'; +export { TitleAndAbstractStepComponent } from './submit-steps/title-and-abstract-step/title-and-abstract-step.component'; export { AdvisoryBoardComponent } from '@osf/features/preprints/components/advisory-board/advisory-board.component'; export { PreprintsCreatorsFilterComponent } from '@osf/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component'; export { PreprintsDateCreatedFilterComponent } from '@osf/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component'; diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html new file mode 100644 index 000000000..22aa28654 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html @@ -0,0 +1,41 @@ +

Title and Abstract

+ + +
+
+ + +
+ +
+ + +
+
+
+ +
+ + +
diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.spec.ts b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.spec.ts new file mode 100644 index 000000000..96faef97a --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; + +describe('TitleAndAbstractStepComponent', () => { + let component: TitleAndAbstractStepComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TitleAndAbstractStepComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TitleAndAbstractStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts new file mode 100644 index 000000000..687af1df9 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts @@ -0,0 +1,76 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { InputText } from 'primeng/inputtext'; +import { Textarea } from 'primeng/textarea'; +import { Tooltip } from 'primeng/tooltip'; + +import { ChangeDetectionStrategy, Component, OnInit, output, signal } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; + +import { formInputLimits } from '@osf/features/preprints/constants'; +import { TitleAndAbstractForm } from '@osf/features/preprints/models'; +import { CreatePreprint, SubmitPreprintSelectors } from '@osf/features/preprints/store/submit-preprint'; +import { CustomValidators } from '@shared/utils'; + +@Component({ + selector: 'osf-title-and-abstract-step', + imports: [Card, FormsModule, InputText, Button, Textarea, RouterLink, ReactiveFormsModule, Tooltip], + templateUrl: './title-and-abstract-step.component.html', + styleUrl: './title-and-abstract-step.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TitleAndAbstractStepComponent implements OnInit { + protected titleAndAbstractForm!: FormGroup; + protected inputLimits = formInputLimits; + + private actions = createDispatchMap({ + createPreprint: CreatePreprint, + }); + + createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); + providerId = select(SubmitPreprintSelectors.getSelectedProviderId); + + isCreatingPreprint = signal(false); + nextClicked = output(); + + ngOnInit() { + this.initForm(); + } + + initForm() { + this.titleAndAbstractForm = new FormGroup({ + title: new FormControl(this.createdPreprint()?.title || '', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed()], + }), + abstract: new FormControl(this.createdPreprint()?.description || '', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed(), Validators.minLength(this.inputLimits.abstract.minLength)], + }), + }); + } + + nextButtonClicked() { + if (this.titleAndAbstractForm.invalid) { + return; + } + + const model = this.titleAndAbstractForm.value; + if (!model) { + return; + } + + //TODO if created -> patch + + this.isCreatingPreprint.set(true); + this.actions.createPreprint(model.title!, model.abstract!, this.providerId()!).subscribe({ + next: () => { + this.isCreatingPreprint.set(false); + this.nextClicked.emit(); + }, + }); + } +} diff --git a/src/app/features/preprints/constants/form-input-limits.const.ts b/src/app/features/preprints/constants/form-input-limits.const.ts new file mode 100644 index 000000000..f81482517 --- /dev/null +++ b/src/app/features/preprints/constants/form-input-limits.const.ts @@ -0,0 +1,5 @@ +export const formInputLimits = { + abstract: { + minLength: 20, + }, +}; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index aff80c2ee..5a6bc9f4e 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -1,4 +1,6 @@ import { + Preprint, + PreprintJsonApi, PreprintProviderDetails, PreprintProviderDetailsGetResponse, PreprintProviderShortInfo, @@ -53,4 +55,39 @@ export class PreprintsMapper { preprintProviderId: providerId, })); } + + static toCreatePayload(title: string, abstract: string, providerId: string) { + return { + data: { + attributes: { + title: title, + description: abstract, + }, + relationships: { + provider: { + data: { + id: providerId, + type: 'preprint-provider', + }, + }, + }, + type: 'preprints', + }, + }; + } + + static fromPreprintJsonApi(response: PreprintJsonApi): Preprint { + return { + id: response.id, + dateCreated: response.attributes.date_created, + dateModified: response.attributes.date_modified, + title: response.attributes.title, + description: response.attributes.description, + isPublished: response.attributes.is_published, + tags: response.attributes.tags, + isPublic: response.attributes.public, + version: response.attributes.version, + isLatestVersion: response.attributes.is_latest_version, + }; + } } diff --git a/src/app/features/preprints/models/index.ts b/src/app/features/preprints/models/index.ts index 946c7656c..8c262520f 100644 --- a/src/app/features/preprints/models/index.ts +++ b/src/app/features/preprints/models/index.ts @@ -1 +1,2 @@ export * from './preprints.models'; +export * from './submit-preprint-form.models'; diff --git a/src/app/features/preprints/models/submit-preprint-form.models.ts b/src/app/features/preprints/models/submit-preprint-form.models.ts new file mode 100644 index 000000000..dce90a4f3 --- /dev/null +++ b/src/app/features/preprints/models/submit-preprint-form.models.ts @@ -0,0 +1,6 @@ +import { FormControl } from '@angular/forms'; + +export interface TitleAndAbstractForm { + title: FormControl; + abstract: FormControl; +} 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 ab98788c9..e4409c7c1 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 @@ -81,6 +81,7 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { HeaderStyleHelper.resetToDefaults(); BrandService.resetBranding(); BrowserTabHelper.resetToDefaults(); + //TODO reset submit state, delete preprint if created } stepChange(step: number) { diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 9d5981aa8..e945e46a6 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -6,6 +6,7 @@ import { JsonApiService } from '@core/services'; import { JsonApiResponse } from '@osf/core/models'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; import { + PreprintJsonApi, PreprintProviderDetails, PreprintProviderDetailsGetResponse, PreprintProviderShortInfo, @@ -69,4 +70,13 @@ export class PreprintsService { }) ); } + + createPreprint(title: string, abstract: string, providerId: string) { + const payload = PreprintsMapper.toCreatePayload(title, abstract, providerId); + return this.jsonApiService.post(`${environment.apiUrl}/preprints/`, payload).pipe( + map((response) => { + return PreprintsMapper.fromPreprintJsonApi(response); + }) + ); + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index a7235bac2..784f685b2 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -5,3 +5,13 @@ export class SetSelectedPreprintProviderId { constructor(public id: StringOrNull) {} } + +export class CreatePreprint { + static readonly type = '[Submit Preprint] Create Preprint'; + + constructor( + public title: string, + public abstract: string, + public providerId: string + ) {} +} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index 4fa5cc4b4..60da15373 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -1,5 +1,7 @@ import { StringOrNull } from '@core/helpers'; +import { Preprint } from '@osf/features/preprints/models'; export interface SubmitPreprintStateModel { selectedProviderId: StringOrNull; + createdPreprint: Preprint | null; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index ea13a8568..58d27d797 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -7,4 +7,9 @@ export class SubmitPreprintSelectors { static getSelectedProviderId(state: SubmitPreprintStateModel) { return state.selectedProviderId; } + + @Selector([SubmitPreprintState]) + static getCreatedPreprint(state: SubmitPreprintStateModel) { + return state.createdPreprint; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 3c89163f4..bbcd995e5 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,18 +1,39 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { Injectable } from '@angular/core'; +import { tap } from 'rxjs'; -import { SetSelectedPreprintProviderId, SubmitPreprintStateModel } from './'; +import { inject, Injectable } from '@angular/core'; + +import { PreprintsService } from '@osf/features/preprints/services'; + +import { CreatePreprint, SetSelectedPreprintProviderId, SubmitPreprintStateModel } from './'; @State({ name: 'submitPreprint', + defaults: { + selectedProviderId: null, + createdPreprint: null, + }, }) @Injectable() export class SubmitPreprintState { + preprintsService = inject(PreprintsService); + @Action(SetSelectedPreprintProviderId) setSelectedPreprintProviderId(ctx: StateContext, action: SetSelectedPreprintProviderId) { ctx.patchState({ selectedProviderId: action.id, }); } + + @Action(CreatePreprint) + createPreprint(ctx: StateContext, action: CreatePreprint) { + return this.preprintsService.createPreprint(action.title, action.abstract, action.providerId).pipe( + tap((preprint) => { + ctx.patchState({ + createdPreprint: preprint, + }); + }) + ); + } } From 7c425893036808c75f39da9102cfffc6fef594dc Mon Sep 17 00:00:00 2001 From: Roma Date: Wed, 18 Jun 2025 13:58:58 +0300 Subject: [PATCH 16/35] feat(submit-preprint-first-step): Finished title and abstract step --- .../title-and-abstract-step.component.html | 26 +++++++- .../title-and-abstract-step.component.ts | 63 ++++++++++++++----- .../models/submit-preprint-form.models.ts | 2 +- .../preprints/services/preprints.service.ts | 36 ++++++++++- .../submit-preprint.actions.ts | 14 +++++ .../submit-preprint/submit-preprint.state.ts | 36 ++++++++++- 6 files changed, 154 insertions(+), 23 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html index 22aa28654..6a35c8a42 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html @@ -11,11 +11,33 @@

Title and Abstract

autocomplete="off" [formControl]="titleAndAbstractForm.controls['title']" /> + @let titleControl = titleAndAbstractForm.controls['title']; + @if (titleControl.invalid && (titleControl.touched || titleControl.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + }
- + + @let abstractControl = titleAndAbstractForm.controls['description']; + @if (abstractControl.errors?.['required'] && (abstractControl.touched || abstractControl.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + } + @if (abstractControl.errors?.['minlength'] && (abstractControl.touched || abstractControl.dirty)) { + + {{ 'Abstract must be at least 20 characters.' | translate }} + + }
@@ -35,7 +57,7 @@

Title and Abstract

[pTooltip]="titleAndAbstractForm.invalid ? 'Fill in \'Required\' fields to continue' : ''" tooltipPosition="top" [disabled]="titleAndAbstractForm.invalid" - [loading]="isCreatingPreprint()" + [loading]="isUpdatingPreprint()" (click)="nextButtonClicked()" /> diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts index 687af1df9..bcc581ac7 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts @@ -1,23 +1,38 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { InputText } from 'primeng/inputtext'; +import { Message } from 'primeng/message'; import { Textarea } from 'primeng/textarea'; import { Tooltip } from 'primeng/tooltip'; -import { ChangeDetectionStrategy, Component, OnInit, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostListener, OnInit, output, signal } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { formInputLimits } from '@osf/features/preprints/constants'; import { TitleAndAbstractForm } from '@osf/features/preprints/models'; -import { CreatePreprint, SubmitPreprintSelectors } from '@osf/features/preprints/store/submit-preprint'; +import { CreatePreprint, SubmitPreprintSelectors, UpdatePreprint } from '@osf/features/preprints/store/submit-preprint'; +import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; import { CustomValidators } from '@shared/utils'; @Component({ selector: 'osf-title-and-abstract-step', - imports: [Card, FormsModule, InputText, Button, Textarea, RouterLink, ReactiveFormsModule, Tooltip], + imports: [ + Card, + FormsModule, + InputText, + Button, + Textarea, + RouterLink, + ReactiveFormsModule, + Tooltip, + Message, + TranslatePipe, + ], templateUrl: './title-and-abstract-step.component.html', styleUrl: './title-and-abstract-step.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -25,15 +40,17 @@ import { CustomValidators } from '@shared/utils'; export class TitleAndAbstractStepComponent implements OnInit { protected titleAndAbstractForm!: FormGroup; protected inputLimits = formInputLimits; + protected readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; private actions = createDispatchMap({ createPreprint: CreatePreprint, + updatePreprint: UpdatePreprint, }); createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); providerId = select(SubmitPreprintSelectors.getSelectedProviderId); - isCreatingPreprint = signal(false); + isUpdatingPreprint = signal(false); nextClicked = output(); ngOnInit() { @@ -46,7 +63,7 @@ export class TitleAndAbstractStepComponent implements OnInit { nonNullable: true, validators: [CustomValidators.requiredTrimmed()], }), - abstract: new FormControl(this.createdPreprint()?.description || '', { + description: new FormControl(this.createdPreprint()?.description || '', { nonNullable: true, validators: [CustomValidators.requiredTrimmed(), Validators.minLength(this.inputLimits.abstract.minLength)], }), @@ -59,18 +76,32 @@ export class TitleAndAbstractStepComponent implements OnInit { } const model = this.titleAndAbstractForm.value; - if (!model) { - return; - } - //TODO if created -> patch + if (this.createdPreprint()) { + this.isUpdatingPreprint.set(true); + this.actions.updatePreprint(this.createdPreprint()!.id, model).subscribe({ + complete: () => { + this.isUpdatingPreprint.set(false); + this.nextClicked.emit(); + }, + }); + } else { + this.isUpdatingPreprint.set(true); + this.actions.createPreprint(model.title!, model.description!, this.providerId()!).subscribe({ + complete: () => { + this.isUpdatingPreprint.set(false); + this.nextClicked.emit(); + }, + }); + } + } - this.isCreatingPreprint.set(true); - this.actions.createPreprint(model.title!, model.abstract!, this.providerId()!).subscribe({ - next: () => { - this.isCreatingPreprint.set(false); - this.nextClicked.emit(); - }, - }); + @HostListener('window:beforeunload', ['$event']) + public onBeforeUnload($event: BeforeUnloadEvent): boolean { + if (this.createdPreprint() || this.titleAndAbstractForm.value) { + $event.preventDefault(); + return false; + } + return true; } } diff --git a/src/app/features/preprints/models/submit-preprint-form.models.ts b/src/app/features/preprints/models/submit-preprint-form.models.ts index dce90a4f3..2eefdc073 100644 --- a/src/app/features/preprints/models/submit-preprint-form.models.ts +++ b/src/app/features/preprints/models/submit-preprint-form.models.ts @@ -2,5 +2,5 @@ import { FormControl } from '@angular/forms'; export interface TitleAndAbstractForm { title: FormControl; - abstract: FormControl; + description: FormControl; } diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index e945e46a6..e304c1303 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -6,6 +6,7 @@ import { JsonApiService } from '@core/services'; import { JsonApiResponse } from '@osf/core/models'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; import { + Preprint, PreprintJsonApi, PreprintProviderDetails, PreprintProviderDetailsGetResponse, @@ -20,8 +21,13 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class PreprintsService { - jsonApiService = inject(JsonApiService); - baseUrl = `${environment.apiUrl}/providers/preprints/`; + private jsonApiService = inject(JsonApiService); + private baseUrl = `${environment.apiUrl}/providers/preprints/`; + + private domainToApiFieldMap: Record = { + title: 'title', + description: 'description', + }; getPreprintProviderById(id: string): Observable { return this.jsonApiService @@ -79,4 +85,30 @@ export class PreprintsService { }) ); } + + deletePreprint(id: string) { + return this.jsonApiService.delete(`${environment.apiUrl}/preprints/${id}/`); + } + + updatePreprint(id: string, payload: Partial): Observable { + const apiPayload = this.mapPreprintDomainToApiPayload(payload); + + return this.jsonApiService.patch(`${environment.apiUrl}/preprints/${id}/`, { + data: { + type: 'preprints', + id, + attributes: apiPayload, + }, + }); + } + + private mapPreprintDomainToApiPayload(domainPayload: Partial): Partial { + const apiPayload: Record = {}; + Object.entries(domainPayload).forEach(([key, value]) => { + if (value !== undefined && this.domainToApiFieldMap[key]) { + apiPayload[this.domainToApiFieldMap[key]] = value; + } + }); + return apiPayload; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 784f685b2..3d56b7fc1 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -1,4 +1,5 @@ import { StringOrNull } from '@core/helpers'; +import { Preprint } from '@osf/features/preprints/models'; export class SetSelectedPreprintProviderId { static readonly type = '[Submit Preprint] Set Selected Preprint Provider Id'; @@ -15,3 +16,16 @@ export class CreatePreprint { public providerId: string ) {} } + +export class UpdatePreprint { + static readonly type = '[Submit Preprint] Update Preprint'; + + constructor( + public id: string, + public payload: Partial + ) {} +} + +export class ResetStateAndDeletePreprint { + static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; +} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index bbcd995e5..490c86079 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,12 +1,18 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { tap } from 'rxjs'; +import { EMPTY, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { PreprintsService } from '@osf/features/preprints/services'; -import { CreatePreprint, SetSelectedPreprintProviderId, SubmitPreprintStateModel } from './'; +import { + CreatePreprint, + ResetStateAndDeletePreprint, + SetSelectedPreprintProviderId, + SubmitPreprintStateModel, + UpdatePreprint, +} from './'; @State({ name: 'submitPreprint', @@ -36,4 +42,30 @@ export class SubmitPreprintState { }) ); } + + @Action(UpdatePreprint) + updatePreprint(ctx: StateContext, action: UpdatePreprint) { + return this.preprintsService.updatePreprint(action.id, action.payload).pipe( + tap((preprint) => { + ctx.patchState({ + createdPreprint: preprint, + }); + }) + ); + } + + @Action(ResetStateAndDeletePreprint) + resetStateAndDeletePreprint(ctx: StateContext) { + const state = ctx.getState(); + const createdPreprintId = state.createdPreprint?.id; + ctx.setState({ + selectedProviderId: null, + createdPreprint: null, + }); + if (createdPreprintId) { + return this.preprintsService.deletePreprint(createdPreprintId); + } + + return EMPTY; + } } From e17faa78e3167327a6953e4223c6fe09023c5961 Mon Sep 17 00:00:00 2001 From: Roma Date: Thu, 19 Jun 2025 10:08:22 +0300 Subject: [PATCH 17/35] feat(submit-preprint-file-step): Partly implemented layout --- .../file-step/file-step.component.html | 57 +++++++++++++ .../file-step/file-step.component.scss | 14 ++++ .../file-step/file-step.component.spec.ts | 22 +++++ .../file-step/file-step.component.ts | 80 +++++++++++++++++++ src/app/features/preprints/enums/index.ts | 1 + .../enums/preprint-file-source.enum.ts | 5 ++ .../submit-preprint-stepper.component.html | 3 + .../submit-preprint-stepper.component.ts | 13 ++- .../submit-preprint.actions.ts | 7 ++ .../submit-preprint/submit-preprint.model.ts | 2 + .../submit-preprint.selectors.ts | 5 ++ .../submit-preprint/submit-preprint.state.ts | 11 +++ src/assets/styles/_base.scss | 4 + 13 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 src/app/features/preprints/components/submit-steps/file-step/file-step.component.html create mode 100644 src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss create mode 100644 src/app/features/preprints/components/submit-steps/file-step/file-step.component.spec.ts create mode 100644 src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts create mode 100644 src/app/features/preprints/enums/index.ts create mode 100644 src/app/features/preprints/enums/preprint-file-source.enum.ts diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html new file mode 100644 index 000000000..bb5d0f7de --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -0,0 +1,57 @@ +

File

+ +
+

Please Upload your Preprint

+

Note: You cannot switch options once a file is attached.

+
+ +
+ + +
+ +@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Computer) { +
+ + + +
+} + +@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Project) {} + +
+ + +
diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss new file mode 100644 index 000000000..5e75b5e30 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss @@ -0,0 +1,14 @@ +@use "assets/styles/mixins" as mix; + +.file-source-button { + --p-button-secondary-border-color: var(--grey-2); + --p-button-secondary-background: transparent; + --p-button-secondary-hover-background: var(--bg-blue-3); + --p-button-padding-y: 0.75rem; + --p-button-secondary-color: var(--dark-blue-1); + --p-button-secondary-hover-color: var(--dark-blue-1); + + &.active { + --p-button-secondary-background: var(--bg-blue-3); + } +} diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.spec.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.spec.ts new file mode 100644 index 000000000..427fdde9d --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileStepComponent } from './file-step.component'; + +describe('FileStepComponent', () => { + let component: FileStepComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileStepComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileStepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts new file mode 100644 index 000000000..5c804288f --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -0,0 +1,80 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { Button } from 'primeng/button'; +import { Tooltip } from 'primeng/tooltip'; + +import { NgClass, TitleCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, HostListener, inject, output } from '@angular/core'; + +import { PreprintFileSource } from '@osf/features/preprints/enums'; +import { SetSelectedPreprintFileSource, SubmitPreprintSelectors } from '@osf/features/preprints/store/submit-preprint'; +import { ProjectFilesService } from '@osf/features/project/files/services'; + +@Component({ + selector: 'osf-file-step', + imports: [Button, TitleCasePipe, NgClass, Tooltip], + templateUrl: './file-step.component.html', + styleUrl: './file-step.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FileStepComponent { + private projectFilesService = inject(ProjectFilesService); + private actions = createDispatchMap({ + setSelectedFileSource: SetSelectedPreprintFileSource, + }); + createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); + providerId = select(SubmitPreprintSelectors.getSelectedProviderId); + nextClicked = output(); + readonly PreprintFileSource = PreprintFileSource; + + isFileSourceSelected = computed(() => { + return this.selectedFileSource() !== PreprintFileSource.None; + }); + selectedFileSource = select(SubmitPreprintSelectors.getSelectedFileSource); + + nextButtonClicked() { + this.nextClicked.emit(); + } + + @HostListener('window:beforeunload', ['$event']) + public onBeforeUnload($event: BeforeUnloadEvent): boolean { + $event.preventDefault(); + return false; + } + + backButtonClicked() { + //todo + } + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) return; + + // this.fileName.set(file.name); + // this.fileIsUploading = true; + // this.projectFilesService + // .uploadFile(file, this.projectId(), null) + // .pipe( + // takeUntilDestroyed(this.destroyRef), + // finalize(() => { + // this.fileIsUploading = false; + // this.fileName.set(''); + // input.value = ''; + // this.updateFilesList(); + // }) + // ) + // .subscribe((event) => { + // if (event.type === HttpEventType.Response) { + // if (event.body) { + // const fileId = event?.body?.data.id; + // this.approveFile(fileId); + // } + // } + // }); + } + + selectFileSource(fileSource: PreprintFileSource) { + this.actions.setSelectedFileSource(fileSource); + } +} diff --git a/src/app/features/preprints/enums/index.ts b/src/app/features/preprints/enums/index.ts new file mode 100644 index 000000000..6958f03ba --- /dev/null +++ b/src/app/features/preprints/enums/index.ts @@ -0,0 +1 @@ +export * from './preprint-file-source.enum'; diff --git a/src/app/features/preprints/enums/preprint-file-source.enum.ts b/src/app/features/preprints/enums/preprint-file-source.enum.ts new file mode 100644 index 000000000..7a5cdc093 --- /dev/null +++ b/src/app/features/preprints/enums/preprint-file-source.enum.ts @@ -0,0 +1,5 @@ +export enum PreprintFileSource { + None = 0, + Computer = 1, + Project = 2, +} 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 57a126d1c..d6f7264c2 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 @@ -33,6 +33,9 @@

{{ 'Add a ' + preprintProvider()!.preprintWor @case (0) { } + @case (1) { + + } @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 e4409c7c1..9ee1edfb4 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 @@ -18,6 +18,7 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; +import { FileStepComponent } from '@osf/features/preprints/components/submit-steps/file-step/file-step.component'; import { submitPreprintSteps } from '@osf/features/preprints/constants'; import { BrandService } from '@osf/features/preprints/services'; import { @@ -25,13 +26,16 @@ import { GetPreprintProviderById, PreprintsSelectors, } from '@osf/features/preprints/store/preprints'; -import { SetSelectedPreprintProviderId } from '@osf/features/preprints/store/submit-preprint'; +import { + ResetStateAndDeletePreprint, + SetSelectedPreprintProviderId, +} from '@osf/features/preprints/store/submit-preprint'; import { StepperComponent } from '@shared/components/stepper/stepper.component'; import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/utils'; @Component({ selector: 'osf-submit-preprint-stepper', - imports: [Skeleton, StepperComponent, TitleAndAbstractStepComponent], + imports: [Skeleton, StepperComponent, TitleAndAbstractStepComponent, FileStepComponent], templateUrl: './submit-preprint-stepper.component.html', styleUrl: './submit-preprint-stepper.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -47,13 +51,14 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { getPreprintProviderById: GetPreprintProviderById, getHighlightedSubjectsByProviderId: GetHighlightedSubjectsByProviderId, setSelectedPreprintProviderId: SetSelectedPreprintProviderId, + resetStateAndDeletePreprint: ResetStateAndDeletePreprint, }); readonly submitPreprintSteps = submitPreprintSteps; preprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.providerId())); isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); - currentStep = signal(0); + currentStep = signal(1); isWeb = toSignal(inject(IS_WEB)); constructor() { @@ -81,7 +86,7 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { HeaderStyleHelper.resetToDefaults(); BrandService.resetBranding(); BrowserTabHelper.resetToDefaults(); - //TODO reset submit state, delete preprint if created + this.actions.resetStateAndDeletePreprint(); } stepChange(step: number) { diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 3d56b7fc1..3189c0dd9 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -1,4 +1,5 @@ import { StringOrNull } from '@core/helpers'; +import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; export class SetSelectedPreprintProviderId { @@ -26,6 +27,12 @@ export class UpdatePreprint { ) {} } +export class SetSelectedPreprintFileSource { + static readonly type = '[Submit Preprint] Set Selected Preprint File Source'; + + constructor(public fileSource: PreprintFileSource) {} +} + export class ResetStateAndDeletePreprint { static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index 60da15373..663152431 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -1,7 +1,9 @@ import { StringOrNull } from '@core/helpers'; +import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; export interface SubmitPreprintStateModel { selectedProviderId: StringOrNull; createdPreprint: Preprint | null; + fileSource: PreprintFileSource; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index 58d27d797..77eeb4873 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -12,4 +12,9 @@ export class SubmitPreprintSelectors { static getCreatedPreprint(state: SubmitPreprintStateModel) { return state.createdPreprint; } + + @Selector([SubmitPreprintState]) + static getSelectedFileSource(state: SubmitPreprintStateModel) { + return state.fileSource; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 490c86079..eb6af0781 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -4,11 +4,13 @@ import { EMPTY, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { PreprintFileSource } from '@osf/features/preprints/enums'; import { PreprintsService } from '@osf/features/preprints/services'; import { CreatePreprint, ResetStateAndDeletePreprint, + SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprintStateModel, UpdatePreprint, @@ -19,6 +21,7 @@ import { defaults: { selectedProviderId: null, createdPreprint: null, + fileSource: PreprintFileSource.None, }, }) @Injectable() @@ -61,6 +64,7 @@ export class SubmitPreprintState { ctx.setState({ selectedProviderId: null, createdPreprint: null, + fileSource: PreprintFileSource.None, }); if (createdPreprintId) { return this.preprintsService.deletePreprint(createdPreprintId); @@ -68,4 +72,11 @@ export class SubmitPreprintState { return EMPTY; } + + @Action(SetSelectedPreprintFileSource) + setSelectedPreprintFileSource(ctx: StateContext, action: SetSelectedPreprintFileSource) { + ctx.patchState({ + fileSource: action.fileSource, + }); + } } diff --git a/src/assets/styles/_base.scss b/src/assets/styles/_base.scss index f5837d364..3186e37a4 100644 --- a/src/assets/styles/_base.scss +++ b/src/assets/styles/_base.scss @@ -90,3 +90,7 @@ outline: none; } } + +.cursor-not-allowed { + cursor: not-allowed; +} From 13b41755b9da4561ff8630c7c4cf5d55ec41ff8e Mon Sep 17 00:00:00 2001 From: Roma Date: Thu, 19 Jun 2025 16:50:20 +0300 Subject: [PATCH 18/35] feat(submit-preprint-file-step): Implemented file upload from computer --- .../file-step/file-step.component.html | 41 +++++-- .../file-step/file-step.component.scss | 8 ++ .../file-step/file-step.component.ts | 72 ++++++------ .../preprints/mappers/preprints.mapper.ts | 5 +- .../preprints/models/preprints.models.ts | 64 ++++++----- .../preprints/services/preprints.service.ts | 55 +++++++++- .../submit-preprint.actions.ts | 14 +++ .../submit-preprint/submit-preprint.model.ts | 6 +- .../submit-preprint.selectors.ts | 15 +++ .../submit-preprint/submit-preprint.state.ts | 103 +++++++++++++++++- .../files/services/project-files.service.ts | 13 +++ 11 files changed, 312 insertions(+), 84 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index bb5d0f7de..e5b1649f0 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -36,16 +36,37 @@

File

@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Computer) {
- - - + @if (!fileUploadLink()) { + + } @else if (!preprintFiles().length) { + + + + } +
+ +
+ @if (arePreprintFilesLoading()) { + + } @else { + @for (file of preprintFiles(); track file.id) { +
+
+ +

{{ file.name }}

+
+ + +
+ } + }
} diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss index 5e75b5e30..f0b6fd3d1 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss @@ -12,3 +12,11 @@ --p-button-secondary-background: var(--bg-blue-3); } } + +.file-row { + @include mix.flex-center-between; + margin-top: mix.rem(48px); + padding: mix.rem(6px) mix.rem(12px); + border-bottom: 1px solid var(--grey-2); + border-top: 1px solid var(--grey-2); +} diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index 5c804288f..07a7270ff 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -1,80 +1,76 @@ import { createDispatchMap, select } from '@ngxs/store'; import { Button } from 'primeng/button'; +import { Skeleton } from 'primeng/skeleton'; import { Tooltip } from 'primeng/tooltip'; import { NgClass, TitleCasePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, HostListener, inject, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, HostListener, OnInit, output } from '@angular/core'; import { PreprintFileSource } from '@osf/features/preprints/enums'; -import { SetSelectedPreprintFileSource, SubmitPreprintSelectors } from '@osf/features/preprints/store/submit-preprint'; -import { ProjectFilesService } from '@osf/features/project/files/services'; +import { + GetPreprintFilesLinks, + SetSelectedPreprintFileSource, + SubmitPreprintSelectors, + UploadFile, +} from '@osf/features/preprints/store/submit-preprint'; +import { IconComponent } from '@shared/components'; @Component({ selector: 'osf-file-step', - imports: [Button, TitleCasePipe, NgClass, Tooltip], + imports: [Button, TitleCasePipe, NgClass, Tooltip, Skeleton, IconComponent], templateUrl: './file-step.component.html', styleUrl: './file-step.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FileStepComponent { - private projectFilesService = inject(ProjectFilesService); +export class FileStepComponent implements OnInit { private actions = createDispatchMap({ setSelectedFileSource: SetSelectedPreprintFileSource, + getPreprintFilesLinks: GetPreprintFilesLinks, + uploadFile: UploadFile, }); - createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); + + readonly PreprintFileSource = PreprintFileSource; + providerId = select(SubmitPreprintSelectors.getSelectedProviderId); + selectedFileSource = select(SubmitPreprintSelectors.getSelectedFileSource); + fileUploadLink = select(SubmitPreprintSelectors.getUploadLink); + preprintFiles = select(SubmitPreprintSelectors.getPreprintFiles); + arePreprintFilesLoading = select(SubmitPreprintSelectors.arePreprintFilesLoading); + nextClicked = output(); - readonly PreprintFileSource = PreprintFileSource; isFileSourceSelected = computed(() => { return this.selectedFileSource() !== PreprintFileSource.None; }); - selectedFileSource = select(SubmitPreprintSelectors.getSelectedFileSource); - nextButtonClicked() { - this.nextClicked.emit(); + ngOnInit() { + this.actions.getPreprintFilesLinks(); } - @HostListener('window:beforeunload', ['$event']) - public onBeforeUnload($event: BeforeUnloadEvent): boolean { - $event.preventDefault(); - return false; + selectFileSource(fileSource: PreprintFileSource) { + this.actions.setSelectedFileSource(fileSource); } backButtonClicked() { //todo } + nextButtonClicked() { + this.nextClicked.emit(); + } + onFileSelected(event: Event): void { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) return; - // this.fileName.set(file.name); - // this.fileIsUploading = true; - // this.projectFilesService - // .uploadFile(file, this.projectId(), null) - // .pipe( - // takeUntilDestroyed(this.destroyRef), - // finalize(() => { - // this.fileIsUploading = false; - // this.fileName.set(''); - // input.value = ''; - // this.updateFilesList(); - // }) - // ) - // .subscribe((event) => { - // if (event.type === HttpEventType.Response) { - // if (event.body) { - // const fileId = event?.body?.data.id; - // this.approveFile(fileId); - // } - // } - // }); + this.actions.uploadFile(file); } - selectFileSource(fileSource: PreprintFileSource) { - this.actions.setSelectedFileSource(fileSource); + @HostListener('window:beforeunload', ['$event']) + public onBeforeUnload($event: BeforeUnloadEvent): boolean { + $event.preventDefault(); + return false; } } diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 5a6bc9f4e..a30b1fd87 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -1,9 +1,11 @@ +import { ApiData } from '@core/models'; import { Preprint, PreprintJsonApi, PreprintProviderDetails, PreprintProviderDetailsGetResponse, PreprintProviderShortInfo, + PreprintsRelationshipsJsonApi, Subject, SubjectGetResponse, } from '@osf/features/preprints/models'; @@ -76,7 +78,7 @@ export class PreprintsMapper { }; } - static fromPreprintJsonApi(response: PreprintJsonApi): Preprint { + static fromPreprintJsonApi(response: ApiData): Preprint { return { id: response.id, dateCreated: response.attributes.date_created, @@ -88,6 +90,7 @@ export class PreprintsMapper { isPublic: response.attributes.public, version: response.attributes.version, isLatestVersion: response.attributes.is_latest_version, + primaryFileId: response.relationships.primary_file?.links?.related?.href || null, }; } } diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 2032cc511..0f52fc19c 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -53,6 +53,12 @@ export interface Preprint { isPublic: boolean; version: number; isLatestVersion: boolean; + primaryFileId: StringOrNull; +} + +export interface PreprintFilesLinks { + filesLink: string; + uploadFileLink: string; } //api models @@ -107,31 +113,37 @@ export interface SubjectGetResponse { } export interface PreprintJsonApi { - id: string; - type: 'preprints'; - attributes: { - date_created: string; - date_modified: string; - date_published: Date | null; - original_publication_date: Date | null; - custom_publication_citation: StringOrNull; - doi: StringOrNull; - preprint_doi_created: Date | null; - title: string; - description: string; - is_published: boolean; - is_preprint_orphan: boolean; - license_record: StringOrNull; - tags: string[]; - date_withdrawn: Date | null; - current_user_permissions: string[]; - public: boolean; - reviews_state: string; - date_last_transitioned: Date | null; - version: number; - is_latest_version: boolean; - has_coi: boolean; - conflict_of_interest_statement: StringOrNull; - has_data_links: boolean; + date_created: string; + date_modified: string; + date_published: Date | null; + original_publication_date: Date | null; + custom_publication_citation: StringOrNull; + doi: StringOrNull; + preprint_doi_created: Date | null; + title: string; + description: string; + is_published: boolean; + is_preprint_orphan: boolean; + license_record: StringOrNull; + tags: string[]; + date_withdrawn: Date | null; + current_user_permissions: string[]; + public: boolean; + reviews_state: string; + date_last_transitioned: Date | null; + version: number; + is_latest_version: boolean; + has_coi: boolean; + conflict_of_interest_statement: StringOrNull; + has_data_links: boolean; +} + +export interface PreprintsRelationshipsJsonApi { + primary_file: { + links: { + related: { + href: string; + }; + }; }; } diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index e304c1303..6e5f2fe30 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -3,17 +3,20 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services'; -import { JsonApiResponse } from '@osf/core/models'; +import { ApiData, JsonApiResponse } from '@osf/core/models'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; import { Preprint, + PreprintFilesLinks, PreprintJsonApi, PreprintProviderDetails, PreprintProviderDetailsGetResponse, PreprintProviderShortInfo, + PreprintsRelationshipsJsonApi, Subject, SubjectGetResponse, } from '@osf/features/preprints/models'; +import { FileLinks, FileRelationshipsResponse, FileResponse } from '@osf/features/project/files/models'; import { environment } from 'src/environments/environment'; @@ -79,11 +82,15 @@ export class PreprintsService { createPreprint(title: string, abstract: string, providerId: string) { const payload = PreprintsMapper.toCreatePayload(title, abstract, providerId); - return this.jsonApiService.post(`${environment.apiUrl}/preprints/`, payload).pipe( - map((response) => { - return PreprintsMapper.fromPreprintJsonApi(response); - }) - ); + return this.jsonApiService + .post< + ApiData + >(`${environment.apiUrl}/preprints/`, payload) + .pipe( + map((response) => { + return PreprintsMapper.fromPreprintJsonApi(response); + }) + ); } deletePreprint(id: string) { @@ -102,6 +109,42 @@ export class PreprintsService { }); } + updateFileRelationship(preprintId: string, fileId: string): Observable { + return this.jsonApiService.patch(`${environment.apiUrl}/preprints/${preprintId}/`, { + data: { + type: 'preprints', + id: preprintId, + attributes: {}, + relationships: { + primary_file: { + data: { + type: 'files', + id: fileId, + }, + }, + }, + }, + }); + } + + getPreprintFilesLinks(id: string): Observable { + return this.jsonApiService + .get< + JsonApiResponse[], null> + >(`${environment.apiUrl}/preprints/${id}/files/`) + .pipe( + map((response) => { + const rel = response.data[0].relationships; + const links = response.data[0].links; + + return { + filesLink: rel.files.links.related.href, + uploadFileLink: links.upload, + }; + }) + ); + } + private mapPreprintDomainToApiPayload(domainPayload: Partial): Partial { const apiPayload: Record = {}; Object.entries(domainPayload).forEach(([key, value]) => { diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 3189c0dd9..1a60c6820 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -33,6 +33,20 @@ export class SetSelectedPreprintFileSource { constructor(public fileSource: PreprintFileSource) {} } +export class GetPreprintFilesLinks { + static readonly type = '[Submit Preprint] Get Preprint Files Links'; +} + +export class UploadFile { + static readonly type = '[Submit Preprint] Upload File'; + + constructor(public file: File) {} +} + +export class GetPreprintFiles { + static readonly type = '[Submit Preprint] Get Preprint Files'; +} + export class ResetStateAndDeletePreprint { static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index 663152431..3257af1e7 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -1,9 +1,13 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; -import { Preprint } from '@osf/features/preprints/models'; +import { Preprint, PreprintFilesLinks } from '@osf/features/preprints/models'; +import { OsfFile } from '@osf/features/project/files/models'; +import { AsyncStateModel } from '@shared/models'; export interface SubmitPreprintStateModel { selectedProviderId: StringOrNull; createdPreprint: Preprint | null; fileSource: PreprintFileSource; + preprintFilesLinks: PreprintFilesLinks | null; + preprintFiles: AsyncStateModel; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index 77eeb4873..f2e0aeeab 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -17,4 +17,19 @@ export class SubmitPreprintSelectors { static getSelectedFileSource(state: SubmitPreprintStateModel) { return state.fileSource; } + + @Selector([SubmitPreprintState]) + static getUploadLink(state: SubmitPreprintStateModel) { + return state.preprintFilesLinks?.uploadFileLink; + } + + @Selector([SubmitPreprintState]) + static getPreprintFiles(state: SubmitPreprintStateModel) { + return state.preprintFiles.data; + } + + @Selector([SubmitPreprintState]) + static arePreprintFilesLoading(state: SubmitPreprintStateModel) { + return state.preprintFiles.isLoading; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index eb6af0781..8e3832543 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,19 +1,28 @@ import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; -import { EMPTY, tap } from 'rxjs'; +import { EMPTY, take, tap, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { HttpEventType } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { PreprintFileSource } from '@osf/features/preprints/enums'; +import { Preprint } from '@osf/features/preprints/models'; import { PreprintsService } from '@osf/features/preprints/services'; +import { OsfFile } from '@osf/features/project/files/models'; +import { ProjectFilesService } from '@osf/features/project/files/services'; import { CreatePreprint, + GetPreprintFiles, + GetPreprintFilesLinks, ResetStateAndDeletePreprint, SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprintStateModel, UpdatePreprint, + UploadFile, } from './'; @State({ @@ -22,11 +31,18 @@ import { selectedProviderId: null, createdPreprint: null, fileSource: PreprintFileSource.None, + preprintFilesLinks: null, + preprintFiles: { + data: [], + isLoading: false, + error: null, + }, }, }) @Injectable() export class SubmitPreprintState { - preprintsService = inject(PreprintsService); + private preprintsService = inject(PreprintsService); + private fileService = inject(ProjectFilesService); @Action(SetSelectedPreprintProviderId) setSelectedPreprintProviderId(ctx: StateContext, action: SetSelectedPreprintProviderId) { @@ -57,6 +73,83 @@ export class SubmitPreprintState { ); } + @Action(GetPreprintFilesLinks) + getPreprintFilesLinks(ctx: StateContext) { + const state = ctx.getState(); + if (!state.createdPreprint) { + return EMPTY; + } + return this.preprintsService.getPreprintFilesLinks(state.createdPreprint.id).pipe( + tap((preprintStorage) => { + ctx.patchState({ + preprintFilesLinks: preprintStorage, + }); + }) + ); + } + + @Action(UploadFile) + uploadFile(ctx: StateContext, action: UploadFile) { + const state = ctx.getState(); + if (!state.preprintFilesLinks?.uploadFileLink) { + return EMPTY; + } + return this.fileService.uploadFileByLink(action.file, state.preprintFilesLinks.uploadFileLink).pipe( + tap((event) => { + if (event.type === HttpEventType.Response) { + ctx.dispatch(GetPreprintFiles); + this.preprintsService + .updateFileRelationship(state.createdPreprint!.id, event.body!.data.id) + .pipe( + tap((preprint: Preprint) => { + ctx.setState((state: SubmitPreprintStateModel) => ({ + ...state, + createdPreprint: state.createdPreprint + ? { ...state.createdPreprint, primaryFileId: preprint.primaryFileId } + : null, + })); + }), + take(1) + ) + .subscribe(); + } + }) + ); + } + + @Action(GetPreprintFiles) + getPreprintFiles(ctx: StateContext) { + const state = ctx.getState(); + if (!state.preprintFilesLinks?.filesLink) { + return EMPTY; + } + ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); + + return this.fileService.getFilesWithoutFiltering(state.preprintFilesLinks.filesLink).pipe( + tap((files: OsfFile[]) => { + ctx.setState( + patch({ + preprintFiles: patch({ + data: files, + isLoading: false, + }), + }) + ); + }), + catchError((error) => { + ctx.setState( + patch({ + preprintFiles: patch({ + isLoading: false, + error: error.message, + }), + }) + ); + return throwError(() => error); + }) + ); + } + @Action(ResetStateAndDeletePreprint) resetStateAndDeletePreprint(ctx: StateContext) { const state = ctx.getState(); @@ -65,6 +158,12 @@ export class SubmitPreprintState { selectedProviderId: null, createdPreprint: null, fileSource: PreprintFileSource.None, + preprintFilesLinks: null, + preprintFiles: { + data: [], + isLoading: false, + error: null, + }, }); if (createdPreprintId) { return this.preprintsService.deletePreprint(createdPreprintId); diff --git a/src/app/features/project/files/services/project-files.service.ts b/src/app/features/project/files/services/project-files.service.ts index afd9b9976..2103a5315 100644 --- a/src/app/features/project/files/services/project-files.service.ts +++ b/src/app/features/project/files/services/project-files.service.ts @@ -62,6 +62,10 @@ export class ProjectFilesService { .pipe(map((response) => MapFiles(response.data))); } + getFilesWithoutFiltering(filesLink: string): Observable { + return this.#jsonApiService.get(filesLink).pipe(map((response) => MapFiles(response.data))); + } + uploadFile( file: File, projectId: string, @@ -83,6 +87,15 @@ export class ProjectFilesService { return this.#jsonApiService.putFile(link, file, params); } + uploadFileByLink(file: File, uploadLink: string): Observable>> { + const params = { + kind: 'file', + name: file.name, + }; + + return this.#jsonApiService.putFile(uploadLink, file, params); + } + createFolder(projectId: string, folderName: string, parentFolderId?: string): Observable { const params: Record = { kind: 'folder', From 3ca76dec2a753f588ed39603c0aed97c8d343714 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 20 Jun 2025 15:28:58 +0300 Subject: [PATCH 19/35] feat(submit-preprint-file-step): Partly implemented select from osf project tab --- .../file-step/file-step.component.html | 27 +++- .../file-step/file-step.component.ts | 47 ++++++- .../preprints/services/preprints.service.ts | 117 +++++++++++++----- .../submit-preprint.actions.ts | 12 ++ .../submit-preprint/submit-preprint.model.ts | 4 +- .../submit-preprint.selectors.ts | 10 ++ .../submit-preprint/submit-preprint.state.ts | 69 +++++++++++ .../responses/get-files-response.model.ts | 11 ++ src/app/shared/models/id-name.model.ts | 4 + src/app/shared/models/index.ts | 1 + 10 files changed, 265 insertions(+), 37 deletions(-) create mode 100644 src/app/shared/models/id-name.model.ts diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index e5b1649f0..e66224038 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -35,7 +35,7 @@

File

@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Computer) { -
+
@if (!fileUploadLink()) { } @else if (!preprintFiles().length) { @@ -70,7 +70,30 @@

File

} -@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Project) {} +@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Project) { + +
+

This will make your project public, if it is not already

+

The projects and components for which you have admin access are listed below.

+ + +
+
+}
diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index 07a7270ff..9f03f3b85 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -1,15 +1,33 @@ import { createDispatchMap, select } from '@ngxs/store'; import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { Select, SelectChangeEvent } from 'primeng/select'; import { Skeleton } from 'primeng/skeleton'; import { Tooltip } from 'primeng/tooltip'; +import { debounceTime, distinctUntilChanged } from 'rxjs'; + import { NgClass, TitleCasePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, HostListener, OnInit, output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + HostListener, + inject, + OnInit, + output, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { + GetAvailableProjects, GetPreprintFilesLinks, + GetProjectFiles, SetSelectedPreprintFileSource, SubmitPreprintSelectors, UploadFile, @@ -18,7 +36,7 @@ import { IconComponent } from '@shared/components'; @Component({ selector: 'osf-file-step', - imports: [Button, TitleCasePipe, NgClass, Tooltip, Skeleton, IconComponent], + imports: [Button, TitleCasePipe, NgClass, Tooltip, Skeleton, IconComponent, Card, Select, ReactiveFormsModule], templateUrl: './file-step.component.html', styleUrl: './file-step.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -28,7 +46,10 @@ export class FileStepComponent implements OnInit { setSelectedFileSource: SetSelectedPreprintFileSource, getPreprintFilesLinks: GetPreprintFilesLinks, uploadFile: UploadFile, + getAvailableProjects: GetAvailableProjects, + getFilesForSelectedProject: GetProjectFiles, }); + private destroyRef = inject(DestroyRef); readonly PreprintFileSource = PreprintFileSource; @@ -37,6 +58,9 @@ export class FileStepComponent implements OnInit { fileUploadLink = select(SubmitPreprintSelectors.getUploadLink); preprintFiles = select(SubmitPreprintSelectors.getPreprintFiles); arePreprintFilesLoading = select(SubmitPreprintSelectors.arePreprintFilesLoading); + availableProjects = select(SubmitPreprintSelectors.getAvailableProjects); + areAvailableProjectsLoading = select(SubmitPreprintSelectors.areAvailableProjectsLoading); + projectNameControl = new FormControl(null); nextClicked = output(); @@ -50,6 +74,17 @@ export class FileStepComponent implements OnInit { selectFileSource(fileSource: PreprintFileSource) { this.actions.setSelectedFileSource(fileSource); + + if (fileSource === PreprintFileSource.Project) { + this.actions.getAvailableProjects(null); + + this.projectNameControl.valueChanges + .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + if (!value) return; + this.actions.getAvailableProjects(value); + }); + } } backButtonClicked() { @@ -73,4 +108,12 @@ export class FileStepComponent implements OnInit { $event.preventDefault(); return false; } + + selectProject(event: SelectChangeEvent) { + if (!(event.originalEvent instanceof PointerEvent)) { + return; + } + + this.actions.getFilesForSelectedProject(event.value); + } } diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 6e5f2fe30..c6ebd49bf 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -1,7 +1,8 @@ -import { map, Observable } from 'rxjs'; +import { map, Observable, switchMap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { Primitive, StringOrNull } from '@core/helpers'; import { JsonApiService } from '@core/services'; import { ApiData, JsonApiResponse } from '@osf/core/models'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; @@ -16,7 +17,9 @@ import { Subject, SubjectGetResponse, } from '@osf/features/preprints/models'; -import { FileLinks, FileRelationshipsResponse, FileResponse } from '@osf/features/project/files/models'; +import { GetFileResponse, GetFilesResponse, OsfFile } from '@osf/features/project/files/models'; +import { ProjectFilesService } from '@osf/features/project/files/services'; +import { IdName, NodeData } from '@shared/models'; import { environment } from 'src/environments/environment'; @@ -24,6 +27,7 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class PreprintsService { + private filesService = inject(ProjectFilesService); private jsonApiService = inject(JsonApiService); private baseUrl = `${environment.apiUrl}/providers/preprints/`; @@ -100,51 +104,100 @@ export class PreprintsService { updatePreprint(id: string, payload: Partial): Observable { const apiPayload = this.mapPreprintDomainToApiPayload(payload); - return this.jsonApiService.patch(`${environment.apiUrl}/preprints/${id}/`, { - data: { - type: 'preprints', - id, - attributes: apiPayload, - }, - }); + return this.jsonApiService + .patch>( + `${environment.apiUrl}/preprints/${id}/`, + { + data: { + type: 'preprints', + id, + attributes: apiPayload, + }, + } + ) + .pipe( + map((response) => { + return PreprintsMapper.fromPreprintJsonApi(response); + }) + ); } updateFileRelationship(preprintId: string, fileId: string): Observable { - return this.jsonApiService.patch(`${environment.apiUrl}/preprints/${preprintId}/`, { - data: { - type: 'preprints', - id: preprintId, - attributes: {}, - relationships: { - primary_file: { - data: { - type: 'files', - id: fileId, + return this.jsonApiService + .patch>( + `${environment.apiUrl}/preprints/${preprintId}/`, + { + data: { + type: 'preprints', + id: preprintId, + attributes: {}, + relationships: { + primary_file: { + data: { + type: 'files', + id: fileId, + }, + }, }, }, - }, - }, - }); + } + ) + .pipe( + map((response) => { + return PreprintsMapper.fromPreprintJsonApi(response); + }) + ); } getPreprintFilesLinks(id: string): Observable { + return this.jsonApiService.get(`${environment.apiUrl}/preprints/${id}/files/`).pipe( + map((response) => { + const rel = response.data[0].relationships; + const links = response.data[0].links; + + return { + filesLink: rel.files.links.related.href, + uploadFileLink: links.upload, + }; + }) + ); + } + + getAvailableProjects(searchTerm: StringOrNull): Observable { + const params: Record = {}; + params['page'] = 1; + if (searchTerm) { + params['filter[title]'] = searchTerm; + } + return this.jsonApiService - .get< - JsonApiResponse[], null> - >(`${environment.apiUrl}/preprints/${id}/files/`) + .get>(`${environment.apiUrl}/users/me/nodes/`, params) .pipe( map((response) => { - const rel = response.data[0].relationships; - const links = response.data[0].links; - - return { - filesLink: rel.files.links.related.href, - uploadFileLink: links.upload, - }; + return response.data.map((item) => ({ + id: item.id, + name: item.attributes.title, + })); }) ); } + getProjectFiles(projectId: string): Observable { + return this.jsonApiService.get(`${environment.apiUrl}/nodes/${projectId}/files/`).pipe( + switchMap((response: GetFilesResponse) => { + return this.jsonApiService + .get>(response.data[0].relationships.root_folder.links.related.href) + .pipe( + switchMap((fileResponse) => { + return this.filesService.getFilesWithoutFiltering( + fileResponse.data.relationships.files.links.related.href + ); + }) + ); + }) + ); + } + private mapPreprintDomainToApiPayload(domainPayload: Partial): Partial { const apiPayload: Record = {}; Object.entries(domainPayload).forEach(([key, value]) => { diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 1a60c6820..844ccb994 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -47,6 +47,18 @@ export class GetPreprintFiles { static readonly type = '[Submit Preprint] Get Preprint Files'; } +export class GetAvailableProjects { + static readonly type = '[Submit Preprint] Get Available Projects'; + + constructor(public searchTerm: StringOrNull) {} +} + +export class GetProjectFiles { + static readonly type = '[Submit Preprint] Get Project Files'; + + constructor(public projectId: string) {} +} + export class ResetStateAndDeletePreprint { static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index 3257af1e7..30cc9b0ed 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -2,7 +2,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint, PreprintFilesLinks } from '@osf/features/preprints/models'; import { OsfFile } from '@osf/features/project/files/models'; -import { AsyncStateModel } from '@shared/models'; +import { AsyncStateModel, IdName } from '@shared/models'; export interface SubmitPreprintStateModel { selectedProviderId: StringOrNull; @@ -10,4 +10,6 @@ export interface SubmitPreprintStateModel { fileSource: PreprintFileSource; preprintFilesLinks: PreprintFilesLinks | null; preprintFiles: AsyncStateModel; + availableProjects: AsyncStateModel; + projectFiles: AsyncStateModel; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index f2e0aeeab..80569749f 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -32,4 +32,14 @@ export class SubmitPreprintSelectors { static arePreprintFilesLoading(state: SubmitPreprintStateModel) { return state.preprintFiles.isLoading; } + + @Selector([SubmitPreprintState]) + static getAvailableProjects(state: SubmitPreprintStateModel) { + return state.availableProjects.data; + } + + @Selector([SubmitPreprintState]) + static areAvailableProjectsLoading(state: SubmitPreprintStateModel) { + return state.availableProjects.isLoading; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 8e3832543..878710458 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -15,8 +15,10 @@ import { ProjectFilesService } from '@osf/features/project/files/services'; import { CreatePreprint, + GetAvailableProjects, GetPreprintFiles, GetPreprintFilesLinks, + GetProjectFiles, ResetStateAndDeletePreprint, SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, @@ -37,6 +39,16 @@ import { isLoading: false, error: null, }, + availableProjects: { + data: [], + isLoading: false, + error: null, + }, + projectFiles: { + data: [], + isLoading: false, + error: null, + }, }, }) @Injectable() @@ -150,6 +162,53 @@ export class SubmitPreprintState { ); } + @Action(GetAvailableProjects) + getAvailableProjects(ctx: StateContext, action: GetAvailableProjects) { + ctx.setState(patch({ availableProjects: patch({ isLoading: true }) })); + + return this.preprintsService.getAvailableProjects(action.searchTerm).pipe( + tap((projects) => { + ctx.setState( + patch({ + availableProjects: patch({ + data: projects, + isLoading: false, + }), + }) + ); + }), + catchError((error) => { + ctx.setState( + patch({ + availableProjects: patch({ + isLoading: false, + error: error.message, + }), + }) + ); + return throwError(() => error); + }) + ); + } + + @Action(GetProjectFiles) + getProjectFiles(ctx: StateContext, action: GetProjectFiles) { + ctx.setState(patch({ projectFiles: patch({ isLoading: true }) })); + + return this.preprintsService.getProjectFiles(action.projectId).pipe( + tap((files: OsfFile[]) => { + ctx.setState( + patch({ + projectFiles: patch({ + data: files, + isLoading: false, + }), + }) + ); + }) + ); + } + @Action(ResetStateAndDeletePreprint) resetStateAndDeletePreprint(ctx: StateContext) { const state = ctx.getState(); @@ -164,6 +223,16 @@ export class SubmitPreprintState { isLoading: false, error: null, }, + availableProjects: { + data: [], + isLoading: false, + error: null, + }, + projectFiles: { + data: [], + isLoading: false, + error: null, + }, }); if (createdPreprintId) { return this.preprintsService.deletePreprint(createdPreprintId); diff --git a/src/app/features/project/files/models/responses/get-files-response.model.ts b/src/app/features/project/files/models/responses/get-files-response.model.ts index 954091783..8eafcfe40 100644 --- a/src/app/features/project/files/models/responses/get-files-response.model.ts +++ b/src/app/features/project/files/models/responses/get-files-response.model.ts @@ -51,6 +51,17 @@ export interface FileRelationshipsResponse { }; }; }; + root_folder: { + links: { + related: { + href: string; + }; + }; + data: { + id: string; + type: string; + }; + }; } export interface FileLinks { diff --git a/src/app/shared/models/id-name.model.ts b/src/app/shared/models/id-name.model.ts new file mode 100644 index 000000000..cffbdf870 --- /dev/null +++ b/src/app/shared/models/id-name.model.ts @@ -0,0 +1,4 @@ +export interface IdName { + id: string; + name: string; +} diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 0b64b73a5..4221f1b0d 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -5,6 +5,7 @@ export * from './create-project-form.model'; export * from './filter-labels.model'; export * from './filters'; export * from './google-drive-folder.model'; +export * from './id-name.model'; export * from './metadata-field.model'; export * from './nav-item.model'; export * from './node-response.model'; From 53cca7e4392ed0b0ff4fecfca4774302cc820c1a Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 20 Jun 2025 19:19:02 +0300 Subject: [PATCH 20/35] refactor(files-tree): Made files-tree more reusable by passing currentFolder and handling file click --- .../models/files-tree-actions.interface.ts | 12 ++-- .../project-files.component.html | 4 +- .../project-files/project-files.component.ts | 8 ++- .../files-tree/files-tree.component.html | 10 +-- .../files-tree/files-tree.component.ts | 72 +++++++++---------- 5 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/app/features/project/files/models/files-tree-actions.interface.ts b/src/app/features/project/files/models/files-tree-actions.interface.ts index 5ac344842..983a00038 100644 --- a/src/app/features/project/files/models/files-tree-actions.interface.ts +++ b/src/app/features/project/files/models/files-tree-actions.interface.ts @@ -4,12 +4,12 @@ import { OsfFile } from '@shared/models'; export interface FilesTreeActions { setCurrentFolder: (folder: OsfFile | null) => Observable; - setSearch: (search: string) => Observable; - setSort: (sort: string) => Observable; - setFilesIsLoading: (isLoading: boolean) => Observable; + setSearch?: (search: string) => Observable; + setSort?: (sort: string) => Observable; + setFilesIsLoading?: (isLoading: boolean) => Observable; getFiles: (filesLink: string) => Observable; getRootFolderFiles: (projectId: string) => Observable; - deleteEntry: (projectId: string, link: string) => Observable; - renameEntry: (projectId: string, link: string, newName: string) => Observable; - setMoveFileCurrentFolder: (folder: OsfFile | null) => Observable; + deleteEntry?: (projectId: string, link: string) => Observable; + renameEntry?: (projectId: string, link: string, newName: string) => Observable; + setMoveFileCurrentFolder?: (folder: OsfFile | null) => Observable; } diff --git a/src/app/features/project/files/pages/project-files/project-files.component.html b/src/app/features/project/files/pages/project-files/project-files.component.html index a747000ae..92046a31b 100644 --- a/src/app/features/project/files/pages/project-files/project-files.component.html +++ b/src/app/features/project/files/pages/project-files/project-files.component.html @@ -86,13 +86,15 @@ diff --git a/src/app/features/project/files/pages/project-files/project-files.component.ts b/src/app/features/project/files/pages/project-files/project-files.component.ts index 11cf9b7c8..09f6f92cd 100644 --- a/src/app/features/project/files/pages/project-files/project-files.component.ts +++ b/src/app/features/project/files/pages/project-files/project-files.component.ts @@ -16,7 +16,7 @@ import { HttpEventType } from '@angular/common/http'; import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CreateFolderDialogComponent } from '@osf/features/project/files/components'; import { FilesTreeActions } from '@osf/features/project/files/models'; @@ -42,6 +42,7 @@ import { SearchInputComponent, SubHeaderComponent, } from '@shared/components'; +import { OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; @Component({ @@ -74,6 +75,7 @@ export class ProjectFilesComponent { private readonly destroyRef = inject(DestroyRef); private readonly dialogService = inject(DialogService); private readonly translateService = inject(TranslateService); + private readonly router = inject(Router); private readonly actions = createDispatchMap({ createFolder: CreateFolder, deleteEntry: DeleteEntry, @@ -231,4 +233,8 @@ export class ProjectFilesComponent { this.sortControl.setValue(ALL_SORT_OPTIONS[0].value); } } + + navigateToFile(file: OsfFile) { + this.router.navigate([file.guid], { relativeTo: this.activeRoute }); + } } diff --git a/src/app/shared/components/files-tree/files-tree.component.html b/src/app/shared/components/files-tree/files-tree.component.html index 5ebc66fe9..87293b098 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -9,8 +9,8 @@ styleClass="w-full md:w-[30rem]" class="tree-table" (onNodeDrop)="dropNode($event)" - [draggableNodes]="!readonly()" - [droppableNodes]="!readonly()" + [draggableNodes]="!viewOnly()" + [droppableNodes]="!viewOnly()" draggableScope="self" droppableScope="self" > @@ -28,8 +28,8 @@ } @else {
{{ file.dateModified | date: 'MMM d, y hh:mm a' }}
- @if (!readonly()) { + @if (!viewOnly()) {
diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index 5a7c18c61..99b7f1715 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -1,20 +1,17 @@ -import { select } from '@ngxs/store'; - import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { PrimeTemplate } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; import { Tree, TreeNodeDropEvent } from 'primeng/tree'; -import { finalize, firstValueFrom, Observable, switchMap, take, tap } from 'rxjs'; +import { finalize, firstValueFrom, Observable, take } from 'rxjs'; import { DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { MoveFileDialogComponent, RenameFileDialogComponent } from '@osf/features/project/files/components'; import { embedDynamicJs, embedStaticHtml, FilesTreeActions } from '@osf/features/project/files/models'; -import { ProjectFilesSelectors } from '@osf/features/project/files/store'; import { FileMenuType } from '@osf/shared/enums'; import { FileMenuComponent, LoadingSpinnerComponent } from '@shared/components'; import { FileMenuAction, OsfFile } from '@shared/models'; @@ -40,13 +37,14 @@ export class FilesTreeComponent { files = input.required(); projectId = input.required(); actions = input.required(); - readonly = input.required(); - provider = input.required(); + viewOnly = input(true); + provider = input(); isLoading = input(false); + currentFolder = input.required(); + entryFileClicked = output(); folderIsOpening = output(); - protected readonly currentFolder = select(ProjectFilesSelectors.getCurrentFolder); protected readonly nodes = computed(() => { if (this.currentFolder()?.relationships?.parentFolderLink) { return [ @@ -61,19 +59,23 @@ export class FilesTreeComponent { } }); + constructor() { + effect(() => { + const currentFolder = this.currentFolder(); + if (currentFolder) { + this.updateFilesList().subscribe(() => this.folderIsOpening.emit(false)); + } + }); + } + openEntry(file: OsfFile) { if (file.kind === 'file') { - this.router.navigate([file.guid], { relativeTo: this.route }); + this.entryFileClicked.emit(file); } else { - this.actions().setFilesIsLoading(true); + this.actions().setFilesIsLoading?.(true); this.folderIsOpening.emit(true); - this.actions() - .setCurrentFolder(file) - .pipe(take(1)) - .subscribe(() => { - this.updateFilesList().subscribe(() => this.folderIsOpening.emit(false)); - }); + this.actions().setCurrentFolder(file); } } @@ -82,23 +84,17 @@ export class FilesTreeComponent { if (!currentFolder) return; - this.actions().setFilesIsLoading(true); + this.actions().setFilesIsLoading?.(true); this.folderIsOpening.emit(true); this.filesService .getFolder(currentFolder.relationships.parentFolderLink) - .pipe( - take(1), - switchMap((folder) => - this.actions() - .setCurrentFolder(folder) - .pipe( - take(1), - tap(() => this.updateFilesList().subscribe(() => this.folderIsOpening.emit(false))) - ) - ) - ) - .subscribe(); + .pipe(take(1)) + .subscribe({ + next: (folder) => { + this.actions().setCurrentFolder(folder); + }, + }); } onFileMenuAction(action: FileMenuAction, file: OsfFile): void { @@ -175,8 +171,8 @@ export class FilesTreeComponent { } deleteEntry(link: string): void { - this.actions().setFilesIsLoading(true); - this.actions().deleteEntry(this.projectId(), link).pipe(take(1)).subscribe(); + this.actions().setFilesIsLoading?.(true); + this.actions().deleteEntry?.(this.projectId(), link); } confirmRename(file: OsfFile): void { @@ -201,8 +197,8 @@ export class FilesTreeComponent { renameEntry(newName: string, file: OsfFile): void { if (newName.trim() && file.links.upload) { - this.actions().setFilesIsLoading(true); - this.actions().renameEntry(this.projectId(), file.links.upload, newName).pipe(take(1)).subscribe(); + this.actions().setFilesIsLoading?.(true); + this.actions().renameEntry?.(this.projectId(), file.links.upload, newName).pipe(take(1)).subscribe(); } } @@ -222,10 +218,10 @@ export class FilesTreeComponent { const projectId = this.projectId(); if (projectId && folderId) { if (rootFolder) { - const link = this.filesService.getFolderDownloadLink(projectId, this.provider(), '', true); + const link = this.filesService.getFolderDownloadLink(projectId, this.provider()!, '', true); window.open(link, '_blank')?.focus(); } else { - const link = this.filesService.getFolderDownloadLink(projectId, this.provider(), folderId, false); + const link = this.filesService.getFolderDownloadLink(projectId, this.provider()!, folderId, false); window.open(link, '_blank')?.focus(); } } @@ -233,7 +229,7 @@ export class FilesTreeComponent { moveFile(file: OsfFile, action: string): void { this.actions() - .setMoveFileCurrentFolder(this.currentFolder()) + .setMoveFileCurrentFolder?.(this.currentFolder()) .pipe(take(1)) .subscribe(() => { const header = @@ -303,7 +299,7 @@ export class FilesTreeComponent { } async dropFileToFolder(event: TreeNodeDropEvent): Promise { - this.actions().setFilesIsLoading(true); + this.actions().setFilesIsLoading?.(true); const dropNode = event.dropNode as OsfFile; const dragNode = event.dragNode as OsfFile; @@ -325,7 +321,7 @@ export class FilesTreeComponent { } this.filesService - .moveFile(moveLink, path, this.projectId(), this.provider(), 'move') + .moveFile(moveLink, path, this.projectId(), this.provider()!, 'move') .pipe( take(1), finalize(() => { From 68867da98ceba674630400d1219f9522ad004ee1 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 20 Jun 2025 19:21:01 +0300 Subject: [PATCH 21/35] feat(submit-preprint-file-step): Implemented project files viewing --- .../file-step/file-step.component.html | 15 ++++- .../file-step/file-step.component.ts | 60 +++++++++++++++---- .../submit-preprint.actions.ts | 6 ++ .../submit-preprint.selectors.ts | 10 ++++ .../submit-preprint/submit-preprint.state.ts | 19 ++++++ 5 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index e66224038..fd2ce6147 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -82,7 +82,7 @@

File

optionLabel="name" optionValue="id" [formControl]="projectNameControl" - [placeholder]="'Creator name'" + [placeholder]="'Project Title'" class="w-6" [editable]="true" styleClass="m-t-24" @@ -93,6 +93,19 @@

File

/>
+ +
+ @if (selectedProjectId()) { + + } +
}
diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index 9f03f3b85..d25d1892f 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -2,11 +2,12 @@ import { createDispatchMap, select } from '@ngxs/store'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; +import { DialogService } from 'primeng/dynamicdialog'; import { Select, SelectChangeEvent } from 'primeng/select'; import { Skeleton } from 'primeng/skeleton'; import { Tooltip } from 'primeng/tooltip'; -import { debounceTime, distinctUntilChanged } from 'rxjs'; +import { debounceTime, distinctUntilChanged, EMPTY, Observable } from 'rxjs'; import { NgClass, TitleCasePipe } from '@angular/common'; import { @@ -18,6 +19,7 @@ import { inject, OnInit, output, + signal, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @@ -28,17 +30,32 @@ import { GetAvailableProjects, GetPreprintFilesLinks, GetProjectFiles, + GetProjectFilesByLink, SetSelectedPreprintFileSource, SubmitPreprintSelectors, UploadFile, } from '@osf/features/preprints/store/submit-preprint'; -import { IconComponent } from '@shared/components'; +import { FilesTreeActions } from '@osf/features/project/files/models'; +import { FilesTreeComponent, IconComponent } from '@shared/components'; +import { OsfFile } from '@shared/models'; @Component({ selector: 'osf-file-step', - imports: [Button, TitleCasePipe, NgClass, Tooltip, Skeleton, IconComponent, Card, Select, ReactiveFormsModule], + imports: [ + Button, + TitleCasePipe, + NgClass, + Tooltip, + Skeleton, + IconComponent, + Card, + Select, + ReactiveFormsModule, + FilesTreeComponent, + ], templateUrl: './file-step.component.html', styleUrl: './file-step.component.scss', + providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileStepComponent implements OnInit { @@ -48,6 +65,7 @@ export class FileStepComponent implements OnInit { uploadFile: UploadFile, getAvailableProjects: GetAvailableProjects, getFilesForSelectedProject: GetProjectFiles, + getProjectFilesByLink: GetProjectFilesByLink, }); private destroyRef = inject(DestroyRef); @@ -60,8 +78,26 @@ export class FileStepComponent implements OnInit { arePreprintFilesLoading = select(SubmitPreprintSelectors.arePreprintFilesLoading); availableProjects = select(SubmitPreprintSelectors.getAvailableProjects); areAvailableProjectsLoading = select(SubmitPreprintSelectors.areAvailableProjectsLoading); + projectFiles = select(SubmitPreprintSelectors.getProjectFiles); + areProjectFilesLoading = select(SubmitPreprintSelectors.areProjectFilesLoading); + selectedProjectId = signal(null); + currentFolder = signal(null); + projectNameControl = new FormControl(null); + filesTreeActions: FilesTreeActions = { + setCurrentFolder: (folder: OsfFile | null): Observable => { + this.currentFolder.set(folder); + return EMPTY; + }, + getFiles: (filesLink: string): Observable => { + return this.actions.getProjectFilesByLink(filesLink); + }, + getRootFolderFiles: (projectId: string): Observable => { + return this.actions.getFilesForSelectedProject(projectId); + }, + }; + nextClicked = output(); isFileSourceSelected = computed(() => { @@ -70,6 +106,13 @@ export class FileStepComponent implements OnInit { ngOnInit() { this.actions.getPreprintFilesLinks(); + + this.projectNameControl.valueChanges + .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + this.selectedProjectId.set(value); + this.actions.getAvailableProjects(value); + }); } selectFileSource(fileSource: PreprintFileSource) { @@ -77,13 +120,6 @@ export class FileStepComponent implements OnInit { if (fileSource === PreprintFileSource.Project) { this.actions.getAvailableProjects(null); - - this.projectNameControl.valueChanges - .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) - .subscribe((value) => { - if (!value) return; - this.actions.getAvailableProjects(value); - }); } } @@ -116,4 +152,8 @@ export class FileStepComponent implements OnInit { this.actions.getFilesForSelectedProject(event.value); } + + selectProjectFile(file: OsfFile) { + //[RNi] TODO: implement logic of linking preprint to that file + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 844ccb994..f5bdf2edc 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -59,6 +59,12 @@ export class GetProjectFiles { constructor(public projectId: string) {} } +export class GetProjectFilesByLink { + static readonly type = '[Submit Preprint] Get Project Files By Link'; + + constructor(public filesLink: string) {} +} + export class ResetStateAndDeletePreprint { static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index 80569749f..722219e79 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -42,4 +42,14 @@ export class SubmitPreprintSelectors { static areAvailableProjectsLoading(state: SubmitPreprintStateModel) { return state.availableProjects.isLoading; } + + @Selector([SubmitPreprintState]) + static getProjectFiles(state: SubmitPreprintStateModel) { + return state.projectFiles.data; + } + + @Selector([SubmitPreprintState]) + static areProjectFilesLoading(state: SubmitPreprintStateModel) { + return state.projectFiles.isLoading; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index bcea50db0..3d6649ed6 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -19,6 +19,7 @@ import { GetPreprintFiles, GetPreprintFilesLinks, GetProjectFiles, + GetProjectFilesByLink, ResetStateAndDeletePreprint, SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, @@ -209,6 +210,24 @@ export class SubmitPreprintState { ); } + @Action(GetProjectFilesByLink) + getProjectFilesByLink(ctx: StateContext, action: GetProjectFilesByLink) { + ctx.setState(patch({ projectFiles: patch({ isLoading: true }) })); + + return this.fileService.getFilesWithoutFiltering(action.filesLink).pipe( + tap((files: OsfFile[]) => { + ctx.setState( + patch({ + projectFiles: patch({ + data: files, + isLoading: false, + }), + }) + ); + }) + ); + } + @Action(ResetStateAndDeletePreprint) resetStateAndDeletePreprint(ctx: StateContext) { const state = ctx.getState(); From 7c18f45481709c439757bac317682d57de31370a Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 23 Jun 2025 14:32:23 +0300 Subject: [PATCH 22/35] refactor(submit-preprint-stepper): Fixed comments --- .../file-step/file-step.component.html | 2 +- .../file-step/file-step.component.ts | 2 +- .../title-and-abstract-step.component.html | 20 +-- .../title-and-abstract-step.component.ts | 20 +-- .../constants/form-input-limits.const.ts | 4 + .../constants/submit-preprint-steps.const.ts | 59 ++++---- src/app/features/preprints/enums/index.ts | 3 +- .../enums/preprint-file-source.enum.ts | 4 +- .../preprints/enums/submit-steps.enum.ts | 8 ++ .../preprints/models/preprints.models.ts | 2 - .../preprints/services/preprints.service.ts | 20 +-- .../submit-preprint/submit-preprint.model.ts | 4 +- .../submit-preprint.selectors.ts | 9 +- .../submit-preprint/submit-preprint.state.ts | 127 +++++++++++------- .../components/stepper/stepper.component.scss | 4 +- .../components/stepper/stepper.component.ts | 5 +- src/app/shared/models/index.ts | 1 - src/app/shared/models/stepper-step.model.ts | 4 - 18 files changed, 161 insertions(+), 137 deletions(-) create mode 100644 src/app/features/preprints/enums/submit-steps.enum.ts delete mode 100644 src/app/shared/models/stepper-step.model.ts diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index fd2ce6147..cb4ed9f27 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -54,7 +54,7 @@

File

@if (arePreprintFilesLoading()) { - + } @else { @for (file of preprintFiles(); track file.id) {
diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index d25d1892f..451e9a9a5 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -124,7 +124,7 @@ export class FileStepComponent implements OnInit { } backButtonClicked() { - //todo + //[RNi] TODO: implement logic of going back to the previous step } nextButtonClicked() { diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html index 6a35c8a42..517e1b15f 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html @@ -3,20 +3,7 @@

Title and Abstract

- - - @let titleControl = titleAndAbstractForm.controls['title']; - @if (titleControl.invalid && (titleControl.touched || titleControl.dirty)) { - - {{ INPUT_VALIDATION_MESSAGES.required | translate }} - - } +
@@ -38,6 +25,11 @@

Title and Abstract

{{ 'Abstract must be at least 20 characters.' | translate }} } + @if (abstractControl.errors?.['maxlength'] && (abstractControl.touched || abstractControl.dirty)) { + + {{ 'Abstract can not exceed 3000 characters.' | translate }} + + }
diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts index bcc581ac7..ad43bf852 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts @@ -4,18 +4,18 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; -import { InputText } from 'primeng/inputtext'; import { Message } from 'primeng/message'; import { Textarea } from 'primeng/textarea'; import { Tooltip } from 'primeng/tooltip'; -import { ChangeDetectionStrategy, Component, HostListener, OnInit, output, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostListener, OnInit, output } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { formInputLimits } from '@osf/features/preprints/constants'; import { TitleAndAbstractForm } from '@osf/features/preprints/models'; import { CreatePreprint, SubmitPreprintSelectors, UpdatePreprint } from '@osf/features/preprints/store/submit-preprint'; +import { TextInputComponent } from '@shared/components'; import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; import { CustomValidators } from '@shared/utils'; @@ -24,7 +24,6 @@ import { CustomValidators } from '@shared/utils'; imports: [ Card, FormsModule, - InputText, Button, Textarea, RouterLink, @@ -32,6 +31,7 @@ import { CustomValidators } from '@shared/utils'; Tooltip, Message, TranslatePipe, + TextInputComponent, ], templateUrl: './title-and-abstract-step.component.html', styleUrl: './title-and-abstract-step.component.scss', @@ -50,7 +50,7 @@ export class TitleAndAbstractStepComponent implements OnInit { createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); providerId = select(SubmitPreprintSelectors.getSelectedProviderId); - isUpdatingPreprint = signal(false); + isUpdatingPreprint = select(SubmitPreprintSelectors.isPreprintSubmitting); nextClicked = output(); ngOnInit() { @@ -61,11 +61,15 @@ export class TitleAndAbstractStepComponent implements OnInit { this.titleAndAbstractForm = new FormGroup({ title: new FormControl(this.createdPreprint()?.title || '', { nonNullable: true, - validators: [CustomValidators.requiredTrimmed()], + validators: [CustomValidators.requiredTrimmed(), Validators.maxLength(this.inputLimits.title.maxLength)], }), description: new FormControl(this.createdPreprint()?.description || '', { nonNullable: true, - validators: [CustomValidators.requiredTrimmed(), Validators.minLength(this.inputLimits.abstract.minLength)], + validators: [ + CustomValidators.requiredTrimmed(), + Validators.minLength(this.inputLimits.abstract.minLength), + Validators.maxLength(this.inputLimits.abstract.maxLength), + ], }), }); } @@ -78,18 +82,14 @@ export class TitleAndAbstractStepComponent implements OnInit { const model = this.titleAndAbstractForm.value; if (this.createdPreprint()) { - this.isUpdatingPreprint.set(true); this.actions.updatePreprint(this.createdPreprint()!.id, model).subscribe({ complete: () => { - this.isUpdatingPreprint.set(false); this.nextClicked.emit(); }, }); } else { - this.isUpdatingPreprint.set(true); this.actions.createPreprint(model.title!, model.description!, this.providerId()!).subscribe({ complete: () => { - this.isUpdatingPreprint.set(false); this.nextClicked.emit(); }, }); diff --git a/src/app/features/preprints/constants/form-input-limits.const.ts b/src/app/features/preprints/constants/form-input-limits.const.ts index f81482517..23d991b1e 100644 --- a/src/app/features/preprints/constants/form-input-limits.const.ts +++ b/src/app/features/preprints/constants/form-input-limits.const.ts @@ -1,5 +1,9 @@ export const formInputLimits = { abstract: { minLength: 20, + maxLength: 3000, + }, + title: { + maxLength: 100, }, }; diff --git a/src/app/features/preprints/constants/submit-preprint-steps.const.ts b/src/app/features/preprints/constants/submit-preprint-steps.const.ts index 30acd7751..de95b0d01 100644 --- a/src/app/features/preprints/constants/submit-preprint-steps.const.ts +++ b/src/app/features/preprints/constants/submit-preprint-steps.const.ts @@ -1,28 +1,33 @@ -import { StepperStep } from '@shared/models'; +import { SubmitSteps } from '@osf/features/preprints/enums'; +import { CustomOption } from '@shared/models'; -export const submitPreprintSteps: StepperStep[] = [ - { - label: 'Title and Abstract', - value: 0, - }, - { - label: 'File', - value: 1, - }, - { - label: 'Metadata', - value: 2, - }, - { - label: 'Author Assertions', - value: 3, - }, - { - label: 'Supplements', - value: 4, - }, - { - label: 'Review', - value: 5, - }, -]; +export const submitPreprintSteps: CustomOption[] = Object.entries(SubmitSteps) + .filter(([, value]) => typeof value === 'number') + .map(([, value]) => { + let label = ''; + switch (value) { + case SubmitSteps.TitleAndAbstract: + label = 'Title and Abstract'; + break; + case SubmitSteps.File: + label = 'File'; + break; + case SubmitSteps.Metadata: + label = 'Metadata'; + break; + case SubmitSteps.AuthorAssertions: + label = 'Author Assertions'; + break; + case SubmitSteps.Supplements: + label = 'Supplements'; + break; + case SubmitSteps.Review: + label = 'Review'; + break; + } + + return { + label, + value: value as SubmitSteps, + }; + }); diff --git a/src/app/features/preprints/enums/index.ts b/src/app/features/preprints/enums/index.ts index 6958f03ba..5b8a8b7f9 100644 --- a/src/app/features/preprints/enums/index.ts +++ b/src/app/features/preprints/enums/index.ts @@ -1 +1,2 @@ -export * from './preprint-file-source.enum'; +export { PreprintFileSource } from './preprint-file-source.enum'; +export { SubmitSteps } from './submit-steps.enum'; diff --git a/src/app/features/preprints/enums/preprint-file-source.enum.ts b/src/app/features/preprints/enums/preprint-file-source.enum.ts index 7a5cdc093..46310190f 100644 --- a/src/app/features/preprints/enums/preprint-file-source.enum.ts +++ b/src/app/features/preprints/enums/preprint-file-source.enum.ts @@ -1,5 +1,5 @@ export enum PreprintFileSource { None = 0, - Computer = 1, - Project = 2, + Computer, + Project, } diff --git a/src/app/features/preprints/enums/submit-steps.enum.ts b/src/app/features/preprints/enums/submit-steps.enum.ts new file mode 100644 index 000000000..ecdd9a3a1 --- /dev/null +++ b/src/app/features/preprints/enums/submit-steps.enum.ts @@ -0,0 +1,8 @@ +export enum SubmitSteps { + TitleAndAbstract = 0, + File, + Metadata, + AuthorAssertions, + Supplements, + Review, +} diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 0f52fc19c..6d3a1e76f 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -1,6 +1,5 @@ import { StringOrNull } from '@core/helpers'; -// domain models export interface Brand { id: string; name: string; @@ -61,7 +60,6 @@ export interface PreprintFilesLinks { uploadFileLink: string; } -//api models export interface PreprintProviderDetailsGetResponse { id: string; type: 'preprint-providers'; diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 41c5cedfa..117028d1f 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -114,11 +114,7 @@ export class PreprintsService { }, } ) - .pipe( - map((response) => { - return PreprintsMapper.fromPreprintJsonApi(response); - }) - ); + .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } updateFileRelationship(preprintId: string, fileId: string): Observable { @@ -141,11 +137,7 @@ export class PreprintsService { }, } ) - .pipe( - map((response) => { - return PreprintsMapper.fromPreprintJsonApi(response); - }) - ); + .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } getPreprintFilesLinks(id: string): Observable { @@ -187,11 +179,9 @@ export class PreprintsService { return this.jsonApiService .get>(response.data[0].relationships.root_folder.links.related.href) .pipe( - switchMap((fileResponse) => { - return this.filesService.getFilesWithoutFiltering( - fileResponse.data.relationships.files.links.related.href - ); - }) + switchMap((fileResponse) => + this.filesService.getFilesWithoutFiltering(fileResponse.data.relationships.files.links.related.href) + ) ); }) ); diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index cd517cc5c..f26210c44 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -5,9 +5,9 @@ import { AsyncStateModel, IdName, OsfFile } from '@shared/models'; export interface SubmitPreprintStateModel { selectedProviderId: StringOrNull; - createdPreprint: Preprint | null; + createdPreprint: AsyncStateModel; fileSource: PreprintFileSource; - preprintFilesLinks: PreprintFilesLinks | null; + preprintFilesLinks: AsyncStateModel; preprintFiles: AsyncStateModel; availableProjects: AsyncStateModel; projectFiles: AsyncStateModel; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index 722219e79..9267ceb23 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -10,7 +10,12 @@ export class SubmitPreprintSelectors { @Selector([SubmitPreprintState]) static getCreatedPreprint(state: SubmitPreprintStateModel) { - return state.createdPreprint; + return state.createdPreprint.data; + } + + @Selector([SubmitPreprintState]) + static isPreprintSubmitting(state: SubmitPreprintStateModel) { + return state.createdPreprint.isSubmitting; } @Selector([SubmitPreprintState]) @@ -20,7 +25,7 @@ export class SubmitPreprintSelectors { @Selector([SubmitPreprintState]) static getUploadLink(state: SubmitPreprintStateModel) { - return state.preprintFilesLinks?.uploadFileLink; + return state.preprintFilesLinks.data?.uploadFileLink; } @Selector([SubmitPreprintState]) diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 3d6649ed6..7e58709dd 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -32,9 +32,18 @@ import { name: 'submitPreprint', defaults: { selectedProviderId: null, - createdPreprint: null, + createdPreprint: { + data: null, + isLoading: false, + error: null, + isSubmitting: false, + }, fileSource: PreprintFileSource.None, - preprintFilesLinks: null, + preprintFilesLinks: { + data: null, + isLoading: false, + error: null, + }, preprintFiles: { data: [], isLoading: false, @@ -66,37 +75,39 @@ export class SubmitPreprintState { @Action(CreatePreprint) createPreprint(ctx: StateContext, action: CreatePreprint) { + ctx.setState(patch({ createdPreprint: patch({ isSubmitting: true }) })); + return this.preprintsService.createPreprint(action.title, action.abstract, action.providerId).pipe( tap((preprint) => { - ctx.patchState({ - createdPreprint: preprint, - }); - }) + ctx.setState(patch({ createdPreprint: patch({ isSubmitting: false, data: preprint }) })); + }), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) ); } @Action(UpdatePreprint) updatePreprint(ctx: StateContext, action: UpdatePreprint) { + ctx.setState(patch({ createdPreprint: patch({ isSubmitting: true }) })); + return this.preprintsService.updatePreprint(action.id, action.payload).pipe( tap((preprint) => { - ctx.patchState({ - createdPreprint: preprint, - }); - }) + ctx.setState(patch({ createdPreprint: patch({ isSubmitting: false, data: preprint }) })); + }), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) ); } @Action(GetPreprintFilesLinks) getPreprintFilesLinks(ctx: StateContext) { const state = ctx.getState(); - if (!state.createdPreprint) { + if (!state.createdPreprint.data) { return EMPTY; } - return this.preprintsService.getPreprintFilesLinks(state.createdPreprint.id).pipe( + ctx.setState(patch({ preprintFilesLinks: patch({ isLoading: true }) })); + + return this.preprintsService.getPreprintFilesLinks(state.createdPreprint.data.id).pipe( tap((preprintStorage) => { - ctx.patchState({ - preprintFilesLinks: preprintStorage, - }); + ctx.setState(patch({ preprintFilesLinks: patch({ isLoading: false, data: preprintStorage }) })); }) ); } @@ -104,25 +115,32 @@ export class SubmitPreprintState { @Action(UploadFile) uploadFile(ctx: StateContext, action: UploadFile) { const state = ctx.getState(); - if (!state.preprintFilesLinks?.uploadFileLink) { + if (!state.preprintFilesLinks.data?.uploadFileLink) { return EMPTY; } - return this.fileService.uploadFileByLink(action.file, state.preprintFilesLinks.uploadFileLink).pipe( + + ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); + + return this.fileService.uploadFileByLink(action.file, state.preprintFilesLinks.data.uploadFileLink).pipe( tap((event) => { if (event.type === HttpEventType.Response) { ctx.dispatch(GetPreprintFiles); this.preprintsService - .updateFileRelationship(state.createdPreprint!.id, event.body!.data.id) + .updateFileRelationship(state.createdPreprint.data!.id, event.body!.data.id) .pipe( tap((preprint: Preprint) => { ctx.setState((state: SubmitPreprintStateModel) => ({ ...state, - createdPreprint: state.createdPreprint - ? { ...state.createdPreprint, primaryFileId: preprint.primaryFileId } - : null, + createdPreprint: { + ...state.createdPreprint, + data: state.createdPreprint.data + ? { ...state.createdPreprint.data, primaryFileId: preprint.primaryFileId } + : null, + }, })); }), - take(1) + take(1), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) ) .subscribe(); } @@ -133,12 +151,12 @@ export class SubmitPreprintState { @Action(GetPreprintFiles) getPreprintFiles(ctx: StateContext) { const state = ctx.getState(); - if (!state.preprintFilesLinks?.filesLink) { + if (!state.preprintFilesLinks.data?.filesLink) { return EMPTY; } ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); - return this.fileService.getFilesWithoutFiltering(state.preprintFilesLinks.filesLink).pipe( + return this.fileService.getFilesWithoutFiltering(state.preprintFilesLinks.data.filesLink).pipe( tap((files: OsfFile[]) => { ctx.setState( patch({ @@ -149,17 +167,7 @@ export class SubmitPreprintState { }) ); }), - catchError((error) => { - ctx.setState( - patch({ - preprintFiles: patch({ - isLoading: false, - error: error.message, - }), - }) - ); - return throwError(() => error); - }) + catchError((error) => this.handleError(ctx, 'preprintFiles', error)) ); } @@ -178,17 +186,7 @@ export class SubmitPreprintState { }) ); }), - catchError((error) => { - ctx.setState( - patch({ - availableProjects: patch({ - isLoading: false, - error: error.message, - }), - }) - ); - return throwError(() => error); - }) + catchError((error) => this.handleError(ctx, 'availableProjects', error)) ); } @@ -206,7 +204,8 @@ export class SubmitPreprintState { }), }) ); - }) + }), + catchError((error) => this.handleError(ctx, 'projectFiles', error)) ); } @@ -224,19 +223,29 @@ export class SubmitPreprintState { }), }) ); - }) + }), + catchError((error) => this.handleError(ctx, 'projectFiles', error)) ); } @Action(ResetStateAndDeletePreprint) resetStateAndDeletePreprint(ctx: StateContext) { const state = ctx.getState(); - const createdPreprintId = state.createdPreprint?.id; + const createdPreprintId = state.createdPreprint.data?.id; ctx.setState({ selectedProviderId: null, - createdPreprint: null, + createdPreprint: { + data: null, + isLoading: false, + error: null, + isSubmitting: false, + }, fileSource: PreprintFileSource.None, - preprintFilesLinks: null, + preprintFilesLinks: { + data: null, + isLoading: false, + error: null, + }, preprintFiles: { data: [], isLoading: false, @@ -266,4 +275,20 @@ export class SubmitPreprintState { fileSource: action.fileSource, }); } + + private handleError( + ctx: StateContext, + section: keyof SubmitPreprintStateModel, + error: Error + ) { + ctx.patchState({ + [section]: { + ...(ctx.getState()[section] as object), + isLoading: false, + isSubmitting: false, + error: error.message, + }, + }); + return throwError(() => error); + } } diff --git a/src/app/shared/components/stepper/stepper.component.scss b/src/app/shared/components/stepper/stepper.component.scss index 9fbf90628..da4dd05f8 100644 --- a/src/app/shared/components/stepper/stepper.component.scss +++ b/src/app/shared/components/stepper/stepper.component.scss @@ -49,9 +49,9 @@ margin-top: mix.rem(10px); .step-label { - width: 80px; + width: mix.rem(80px); text-align: center; - margin-right: 48px; + margin-right: mix.rem(48px); &:last-child { margin-right: 0; diff --git a/src/app/shared/components/stepper/stepper.component.ts b/src/app/shared/components/stepper/stepper.component.ts index 79dd0f825..3f31b0923 100644 --- a/src/app/shared/components/stepper/stepper.component.ts +++ b/src/app/shared/components/stepper/stepper.component.ts @@ -2,8 +2,9 @@ import { TranslatePipe } from '@ngx-translate/core'; import { ChangeDetectionStrategy, Component, input, model } from '@angular/core'; +import { Primitive } from '@core/helpers'; import { IconComponent } from '@shared/components'; -import { StepperStep } from '@shared/models'; +import { CustomOption } from '@shared/models'; @Component({ selector: 'osf-stepper', @@ -13,7 +14,7 @@ import { StepperStep } from '@shared/models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class StepperComponent { - steps = input.required(); + steps = input.required[]>(); currentStep = model.required(); linear = input(true); diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 0156edcee..aab430c28 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -19,7 +19,6 @@ export * from './resource-card'; export * from './select-option.model'; export * from './severity.type'; export * from './social-icon.model'; -export * from './stepper-step.model'; export * from './store'; export * from './tab-option.model'; export * from './table-parameters.model'; diff --git a/src/app/shared/models/stepper-step.model.ts b/src/app/shared/models/stepper-step.model.ts deleted file mode 100644 index 9199e35a9..000000000 --- a/src/app/shared/models/stepper-step.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface StepperStep { - label: string; - value: number; -} From 8ef907a33efc6d1c1ebe9dc01e1d74dfbb858b5f Mon Sep 17 00:00:00 2001 From: Roma Date: Thu, 26 Jun 2025 21:09:06 +0300 Subject: [PATCH 23/35] feat(submit-preprint-file-step): Implemented selecting file from project --- .../file-step/file-step.component.html | 40 ++++++++--------- .../file-step/file-step.component.scss | 1 - .../file-step/file-step.component.ts | 5 ++- .../submit-preprint.actions.ts | 7 +++ .../submit-preprint/submit-preprint.state.ts | 44 ++++++++++++++++++- src/app/shared/services/files.service.ts | 13 ++++++ 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index cb4ed9f27..8bd713ecc 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -34,11 +34,11 @@

File

/>
-@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Computer) { +@if (selectedFileSource() === PreprintFileSource.Computer) {
@if (!fileUploadLink()) { - } @else if (!preprintFiles().length) { + } @else if (!preprintFiles().length && !arePreprintFilesLoading()) { File

} - -
- @if (arePreprintFilesLoading()) { - - } @else { - @for (file of preprintFiles(); track file.id) { -
-
- -

{{ file.name }}

-
- - -
- } - } -
} -@if (isFileSourceSelected() && selectedFileSource() === PreprintFileSource.Project) { +@if (selectedFileSource() === PreprintFileSource.Project && !preprintFiles().length && !arePreprintFilesLoading()) {

This will make your project public, if it is not already

@@ -108,6 +91,23 @@

File

} +
+ @if (arePreprintFilesLoading()) { + + } @else { + @for (file of preprintFiles(); track file.id) { +
+
+ +

{{ file.name }}

+
+ + +
+ } + } +
+
diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss index f0b6fd3d1..232ef62f4 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss @@ -15,7 +15,6 @@ .file-row { @include mix.flex-center-between; - margin-top: mix.rem(48px); padding: mix.rem(6px) mix.rem(12px); border-bottom: 1px solid var(--grey-2); border-top: 1px solid var(--grey-2); diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index 451e9a9a5..da0e6380f 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -27,6 +27,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { + CopyFileFromProject, GetAvailableProjects, GetPreprintFilesLinks, GetProjectFiles, @@ -66,6 +67,7 @@ export class FileStepComponent implements OnInit { getAvailableProjects: GetAvailableProjects, getFilesForSelectedProject: GetProjectFiles, getProjectFilesByLink: GetProjectFilesByLink, + copyFileFromProject: CopyFileFromProject, }); private destroyRef = inject(DestroyRef); @@ -128,6 +130,7 @@ export class FileStepComponent implements OnInit { } nextButtonClicked() { + //TODO only if primary file id this.nextClicked.emit(); } @@ -154,6 +157,6 @@ export class FileStepComponent implements OnInit { } selectProjectFile(file: OsfFile) { - //[RNi] TODO: implement logic of linking preprint to that file + this.actions.copyFileFromProject(file); } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index f5bdf2edc..9b19e5bf7 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -1,6 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; +import { OsfFile } from '@shared/models'; export class SetSelectedPreprintProviderId { static readonly type = '[Submit Preprint] Set Selected Preprint Provider Id'; @@ -43,6 +44,12 @@ export class UploadFile { constructor(public file: File) {} } +export class CopyFileFromProject { + static readonly type = '[Submit Preprint] Copy File From Project'; + + constructor(public file: OsfFile) {} +} + export class GetPreprintFiles { static readonly type = '[Submit Preprint] Get Preprint Files'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 7e58709dd..a999255aa 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -14,6 +14,7 @@ import { OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; import { + CopyFileFromProject, CreatePreprint, GetAvailableProjects, GetPreprintFiles, @@ -125,8 +126,11 @@ export class SubmitPreprintState { tap((event) => { if (event.type === HttpEventType.Response) { ctx.dispatch(GetPreprintFiles); + + const file = event.body!.data; + const createdFileId = file.id.split('/')[1]; this.preprintsService - .updateFileRelationship(state.createdPreprint.data!.id, event.body!.data.id) + .updateFileRelationship(state.createdPreprint.data!.id, createdFileId) .pipe( tap((preprint: Preprint) => { ctx.setState((state: SubmitPreprintStateModel) => ({ @@ -276,6 +280,44 @@ export class SubmitPreprintState { }); } + @Action(CopyFileFromProject) + copyFileFromProject(ctx: StateContext, action: CopyFileFromProject) { + const createdPreprintId = ctx.getState().createdPreprint.data?.id; + if (!createdPreprintId) { + return; + } + + ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); + + return this.fileService + .copyFileToAnotherLocation(action.file.links.move, action.file.provider, createdPreprintId) + .pipe( + tap((file: OsfFile) => { + ctx.dispatch(GetPreprintFiles); + const fileIdAfterCopy = file.id.split('/')[1]; + this.preprintsService + .updateFileRelationship(createdPreprintId, fileIdAfterCopy) + .pipe( + tap((preprint: Preprint) => { + ctx.setState((state: SubmitPreprintStateModel) => ({ + ...state, + createdPreprint: { + ...state.createdPreprint, + data: state.createdPreprint.data + ? { ...state.createdPreprint.data, primaryFileId: preprint.primaryFileId } + : null, + }, + })); + }), + take(1), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) + ) + .subscribe(); + }), + catchError((error) => this.handleError(ctx, 'preprintFiles', error)) + ); + } + private handleError( ctx: StateContext, section: keyof SubmitPreprintStateModel, diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 9db186a4e..5f358039f 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -240,4 +240,17 @@ export class FilesService { >(`${environment.apiUrl}/files/${fileGuid}/`, payload) .pipe(map((response) => MapFile(response))); } + + copyFileToAnotherLocation(moveLink: string, provider: string, resourceId: string) { + const body = { + action: 'copy', + conflict: 'replace', + path: '/', + provider, + resource: resourceId, + }; + return this.#jsonApiService + .post>(moveLink, body) + .pipe(map((response) => MapFile(response))); + } } From 40ea22d146411de3665157bfe29f7e5fea7cc1cc Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 27 Jun 2025 12:00:12 +0300 Subject: [PATCH 24/35] feat(submit-preprint-file-step): Implemented version file --- .../file-step/file-step.component.html | 35 ++++++++++--------- .../file-step/file-step.component.ts | 33 ++++++++++++++++- .../submit-preprint.actions.ts | 6 ++++ .../submit-preprint/submit-preprint.state.ts | 19 +++++++++- src/app/shared/services/files.service.ts | 12 +++++-- 5 files changed, 84 insertions(+), 21 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index 8bd713ecc..f8ed7753e 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -27,7 +27,7 @@

File

[styleClass]="isFileSourceSelected() ? 'w-full cursor-not-allowed' : 'w-full'" [label]="'Select From existing OSF project' | titlecase" severity="secondary" - [disabled]="isFileSourceSelected()" + [disabled]="isFileSourceSelected() || versionFileMode()" [pTooltip]="isFileSourceSelected() ? 'Start a new preprint to attach a file from your project.' : ''" tooltipPosition="top" (click)="selectFileSource(PreprintFileSource.Project)" @@ -38,7 +38,7 @@

File

@if (!fileUploadLink()) { - } @else if (!preprintFiles().length && !arePreprintFilesLoading()) { + } @else if ((!preprintFiles().length && !arePreprintFilesLoading()) || versionFileMode()) { File }
} +@if (!versionFileMode()) { +
+ @if (arePreprintFilesLoading()) { + + } @else { + @for (file of preprintFiles(); track file.id) { +
+
+ +

{{ file.name }}

+
-
- @if (arePreprintFilesLoading()) { - - } @else { - @for (file of preprintFiles(); track file.id) { -
-
- -

{{ file.name }}

-
- - -
+ +
+ } } - } -
+
+}
diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index da0e6380f..5b69f9fe0 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -1,5 +1,6 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { ConfirmationService } from 'primeng/api'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { DialogService } from 'primeng/dynamicdialog'; @@ -32,6 +33,7 @@ import { GetPreprintFilesLinks, GetProjectFiles, GetProjectFilesByLink, + ReuploadFile, SetSelectedPreprintFileSource, SubmitPreprintSelectors, UploadFile, @@ -39,6 +41,7 @@ import { import { FilesTreeActions } from '@osf/features/project/files/models'; import { FilesTreeComponent, IconComponent } from '@shared/components'; import { OsfFile } from '@shared/models'; +import { defaultConfirmationConfig } from '@shared/utils'; @Component({ selector: 'osf-file-step', @@ -60,10 +63,12 @@ import { OsfFile } from '@shared/models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileStepComponent implements OnInit { + private confirmationService = inject(ConfirmationService); private actions = createDispatchMap({ setSelectedFileSource: SetSelectedPreprintFileSource, getPreprintFilesLinks: GetPreprintFilesLinks, uploadFile: UploadFile, + reuploadFile: ReuploadFile, getAvailableProjects: GetAvailableProjects, getFilesForSelectedProject: GetProjectFiles, getProjectFilesByLink: GetProjectFilesByLink, @@ -85,6 +90,10 @@ export class FileStepComponent implements OnInit { selectedProjectId = signal(null); currentFolder = signal(null); + //TODO fix files tree bug, of showing old files when change project + //TODO check version file for project + versionFileMode = signal(false); + projectNameControl = new FormControl(null); filesTreeActions: FilesTreeActions = { @@ -139,7 +148,12 @@ export class FileStepComponent implements OnInit { const file = input.files?.[0]; if (!file) return; - this.actions.uploadFile(file); + if (this.versionFileMode()) { + this.versionFileMode.set(false); + this.actions.reuploadFile(file); + } else { + this.actions.uploadFile(file); + } } @HostListener('window:beforeunload', ['$event']) @@ -159,4 +173,21 @@ export class FileStepComponent implements OnInit { selectProjectFile(file: OsfFile) { this.actions.copyFileFromProject(file); } + + versionFile() { + this.confirmationService.confirm({ + ...defaultConfirmationConfig, + header: 'Add a new preprint file', + message: + 'This will allow a new version of the preprint file to be uploaded to the preprint. The existing file will be retained as a version of the preprint.', + acceptButtonProps: { + label: 'Continue', + severity: 'danger', + }, + accept: () => { + this.versionFileMode.set(true); + this.actions.setSelectedFileSource(PreprintFileSource.None); + }, + }); + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index 9b19e5bf7..ab3585630 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -44,6 +44,12 @@ export class UploadFile { constructor(public file: File) {} } +export class ReuploadFile { + static readonly type = '[Submit Preprint] Reupload File'; + + constructor(public file: File) {} +} + export class CopyFileFromProject { static readonly type = '[Submit Preprint] Copy File From Project'; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index a999255aa..9bd0d9f9e 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,7 +1,7 @@ import { Action, State, StateContext } from '@ngxs/store'; import { patch } from '@ngxs/store/operators'; -import { EMPTY, take, tap, throwError } from 'rxjs'; +import { EMPTY, switchMap, take, tap, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { HttpEventType } from '@angular/common/http'; @@ -22,6 +22,7 @@ import { GetProjectFiles, GetProjectFilesByLink, ResetStateAndDeletePreprint, + ReuploadFile, SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprintStateModel, @@ -152,6 +153,22 @@ export class SubmitPreprintState { ); } + @Action(ReuploadFile) + reuploadFile(ctx: StateContext, action: ReuploadFile) { + const state = ctx.getState(); + const uploadedFile = state.preprintFiles.data[0]; + if (!uploadedFile) return EMPTY; + + ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); + + return this.fileService.updateFileContent(action.file, uploadedFile.links.upload).pipe( + switchMap(() => this.fileService.renameEntry(uploadedFile.links.upload, action.file.name, 'replace')), + tap(() => { + ctx.dispatch(GetPreprintFiles); + }) + ); + } + @Action(GetPreprintFiles) getPreprintFiles(ctx: StateContext) { const state = ctx.getState(); diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 5f358039f..fc6a8972f 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -100,6 +100,14 @@ export class FilesService { return this.#jsonApiService.putFile(uploadLink, file, params); } + updateFileContent(file: File, link: string) { + const params = { + kind: 'file', + }; + + return this.#jsonApiService.put(link, file, params); + } + createFolder(resourceId: string, provider: string, folderName: string, folderId?: string): Observable { const params: Record = { kind: 'folder', @@ -123,11 +131,11 @@ export class FilesService { return this.#jsonApiService.delete(link); } - renameEntry(link: string, name: string) { + renameEntry(link: string, name: string, conflict = ''): Observable { const body = { action: 'rename', rename: name, - conflict: '', + conflict, }; return this.#jsonApiService.post(link, body); } From 6c7c09c5d75da0d45e354cdfe478e79613abaf14 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 27 Jun 2025 12:01:43 +0300 Subject: [PATCH 25/35] style(submit-preprint-stepper): Adjusted styles for responsive --- .../submit-steps/file-step/file-step.component.html | 9 +++++---- .../submit-steps/file-step/file-step.component.scss | 7 +++++++ .../title-and-abstract-step.component.html | 6 +++--- .../title-and-abstract-step.component.scss | 8 ++++++++ .../title-and-abstract-step.component.ts | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index f8ed7753e..59a733aa3 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -54,7 +54,7 @@

File

} @if (selectedFileSource() === PreprintFileSource.Project && !preprintFiles().length && !arePreprintFilesLoading()) { - +

This will make your project public, if it is not already

The projects and components for which you have admin access are listed below.

@@ -66,7 +66,7 @@

File

optionValue="id" [formControl]="projectNameControl" [placeholder]="'Project Title'" - class="w-6" + class="w-12 md:w-6" [editable]="true" styleClass="m-t-24" appendTo="body" @@ -110,6 +110,7 @@

File

}
- - + +
+[disabled]="!createdPreprint()?.primaryFileId" diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss index 232ef62f4..9ee46185f 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.scss @@ -1,4 +1,5 @@ @use "assets/styles/mixins" as mix; +@use "assets/styles/variables" as var; .file-source-button { --p-button-secondary-border-color: var(--grey-2); @@ -19,3 +20,9 @@ border-bottom: 1px solid var(--grey-2); border-top: 1px solid var(--grey-2); } + +.card { + @media (max-width: var.$breakpoint-sm) { + --p-card-body-padding: 0.75rem; + } +} diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html index 517e1b15f..050d49ed8 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html @@ -1,6 +1,6 @@

Title and Abstract

- +
@@ -36,14 +36,14 @@

Title and Abstract

(); + //[RNi] TODO: Handle back button click ngOnInit() { this.initForm(); } From 5ebc16945ca9a0f8e4046d813b55db9442e81747 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 27 Jun 2025 12:21:08 +0300 Subject: [PATCH 26/35] fix(submit-preprint-stepper): Fixed fetching node's file, next/back logic, fixed update primary file --- .../file-step/file-step.component.html | 9 +- .../file-step/file-step.component.ts | 21 ++-- .../submit-preprint-stepper.component.html | 5 +- .../submit-preprint/submit-preprint.state.ts | 95 ++++++++++--------- 4 files changed, 73 insertions(+), 57 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html index 59a733aa3..aa47143d0 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.html @@ -111,6 +111,11 @@

File

- +
-[disabled]="!createdPreprint()?.primaryFileId" diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index 5b69f9fe0..35d45c2e6 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -78,6 +78,7 @@ export class FileStepComponent implements OnInit { readonly PreprintFileSource = PreprintFileSource; + createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); providerId = select(SubmitPreprintSelectors.getSelectedProviderId); selectedFileSource = select(SubmitPreprintSelectors.getSelectedFileSource); fileUploadLink = select(SubmitPreprintSelectors.getUploadLink); @@ -90,8 +91,6 @@ export class FileStepComponent implements OnInit { selectedProjectId = signal(null); currentFolder = signal(null); - //TODO fix files tree bug, of showing old files when change project - //TODO check version file for project versionFileMode = signal(false); projectNameControl = new FormControl(null); @@ -110,6 +109,7 @@ export class FileStepComponent implements OnInit { }; nextClicked = output(); + backClicked = output(); isFileSourceSelected = computed(() => { return this.selectedFileSource() !== PreprintFileSource.None; @@ -120,9 +120,12 @@ export class FileStepComponent implements OnInit { this.projectNameControl.valueChanges .pipe(debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) - .subscribe((value) => { - this.selectedProjectId.set(value); - this.actions.getAvailableProjects(value); + .subscribe((projectNameOrId) => { + if (this.selectedProjectId() === projectNameOrId) { + return; + } + + this.actions.getAvailableProjects(projectNameOrId); }); } @@ -135,11 +138,14 @@ export class FileStepComponent implements OnInit { } backButtonClicked() { - //[RNi] TODO: implement logic of going back to the previous step + this.backClicked.emit(); } nextButtonClicked() { - //TODO only if primary file id + if (!this.createdPreprint()?.primaryFileId) { + return; + } + this.nextClicked.emit(); } @@ -167,6 +173,7 @@ export class FileStepComponent implements OnInit { return; } + this.selectedProjectId.set(event.value); this.actions.getFilesForSelectedProject(event.value); } 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 d6f7264c2..080c96594 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 @@ -34,7 +34,10 @@

{{ 'Add a ' + preprintProvider()!.preprintWor } @case (1) { - + } @default {

No such step

diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 9bd0d9f9e..2f2c6b4cf 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,7 +1,7 @@ import { Action, State, StateContext } from '@ngxs/store'; import { patch } from '@ngxs/store/operators'; -import { EMPTY, switchMap, take, tap, throwError } from 'rxjs'; +import { EMPTY, filter, switchMap, tap, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { HttpEventType } from '@angular/common/http'; @@ -124,31 +124,26 @@ export class SubmitPreprintState { ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); return this.fileService.uploadFileByLink(action.file, state.preprintFilesLinks.data.uploadFileLink).pipe( - tap((event) => { - if (event.type === HttpEventType.Response) { - ctx.dispatch(GetPreprintFiles); - - const file = event.body!.data; - const createdFileId = file.id.split('/')[1]; - this.preprintsService - .updateFileRelationship(state.createdPreprint.data!.id, createdFileId) - .pipe( - tap((preprint: Preprint) => { - ctx.setState((state: SubmitPreprintStateModel) => ({ - ...state, - createdPreprint: { - ...state.createdPreprint, - data: state.createdPreprint.data - ? { ...state.createdPreprint.data, primaryFileId: preprint.primaryFileId } - : null, - }, - })); - }), - take(1), - catchError((error) => this.handleError(ctx, 'createdPreprint', error)) - ) - .subscribe(); - } + filter((event) => event.type === HttpEventType.Response), + switchMap((event) => { + const file = event.body!.data; + const createdFileId = file.id.split('/')[1]; + ctx.dispatch(new GetPreprintFiles()); + + return this.preprintsService.updateFileRelationship(state.createdPreprint.data!.id, createdFileId).pipe( + tap((preprint: Preprint) => { + ctx.patchState({ + createdPreprint: { + ...ctx.getState().createdPreprint, + data: { + ...ctx.getState().createdPreprint.data!, + primaryFileId: preprint.primaryFileId, + }, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) + ); }) ); } @@ -226,7 +221,16 @@ export class SubmitPreprintState { }) ); }), - catchError((error) => this.handleError(ctx, 'projectFiles', error)) + catchError((error) => { + ctx.setState( + patch({ + preprintFiles: patch({ + data: [], + }), + }) + ); + return this.handleError(ctx, 'projectFiles', error); + }) ); } @@ -305,31 +309,28 @@ export class SubmitPreprintState { } ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); - return this.fileService .copyFileToAnotherLocation(action.file.links.move, action.file.provider, createdPreprintId) .pipe( - tap((file: OsfFile) => { - ctx.dispatch(GetPreprintFiles); + switchMap((file: OsfFile) => { + ctx.dispatch(new GetPreprintFiles()); + const fileIdAfterCopy = file.id.split('/')[1]; - this.preprintsService - .updateFileRelationship(createdPreprintId, fileIdAfterCopy) - .pipe( - tap((preprint: Preprint) => { - ctx.setState((state: SubmitPreprintStateModel) => ({ - ...state, - createdPreprint: { - ...state.createdPreprint, - data: state.createdPreprint.data - ? { ...state.createdPreprint.data, primaryFileId: preprint.primaryFileId } - : null, + + return this.preprintsService.updateFileRelationship(createdPreprintId, fileIdAfterCopy).pipe( + tap((preprint: Preprint) => { + ctx.patchState({ + createdPreprint: { + ...ctx.getState().createdPreprint, + data: { + ...ctx.getState().createdPreprint.data!, + primaryFileId: preprint.primaryFileId, }, - })); - }), - take(1), - catchError((error) => this.handleError(ctx, 'createdPreprint', error)) - ) - .subscribe(); + }, + }); + }), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) + ); }), catchError((error) => this.handleError(ctx, 'preprintFiles', error)) ); From b56899611f201f993dfc603e19b7cdfe218f46bb Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 27 Jun 2025 21:02:04 +0300 Subject: [PATCH 27/35] feat(submit-preprint-metadata-step): Implemented contributors section of metadata step --- .../features/preprints/components/index.ts | 4 +- .../contributors/contributors.component.html | 32 +++ .../contributors/contributors.component.scss | 0 .../contributors.component.spec.ts | 22 ++ .../contributors/contributors.component.ts | 191 ++++++++++++++++++ .../metadata/metadata.component.html | 8 + .../metadata/metadata.component.scss | 0 .../metadata/metadata.component.spec.ts | 22 ++ .../metadata/metadata.component.ts | 14 ++ .../preprints/constants/preprints.routes.ts | 2 + .../submit-preprint-stepper.component.html | 3 + .../submit-preprint-stepper.component.ts | 11 +- .../services/contributors.service.ts | 50 +++++ src/app/features/preprints/services/index.ts | 1 + .../submit-preprint.actions.ts | 23 +++ .../submit-preprint/submit-preprint.model.ts | 2 + .../submit-preprint.selectors.ts | 10 + .../submit-preprint/submit-preprint.state.ts | 103 +++++++++- src/assets/styles/overrides/message.scss | 12 +- 19 files changed, 497 insertions(+), 13 deletions(-) create mode 100644 src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html create mode 100644 src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.scss create mode 100644 src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.spec.ts create mode 100644 src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts create mode 100644 src/app/features/preprints/components/submit-steps/metadata/metadata.component.html create mode 100644 src/app/features/preprints/components/submit-steps/metadata/metadata.component.scss create mode 100644 src/app/features/preprints/components/submit-steps/metadata/metadata.component.spec.ts create mode 100644 src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts create mode 100644 src/app/features/preprints/services/contributors.service.ts diff --git a/src/app/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts index d654f5720..37952fcb1 100644 --- a/src/app/features/preprints/components/index.ts +++ b/src/app/features/preprints/components/index.ts @@ -1,6 +1,5 @@ export { BrowseBySubjectsComponent } from './browse-by-subjects/browse-by-subjects.component'; export { PreprintServicesComponent } from './preprint-services/preprint-services.component'; -export { TitleAndAbstractStepComponent } from './submit-steps/title-and-abstract-step/title-and-abstract-step.component'; export { AdvisoryBoardComponent } from '@osf/features/preprints/components/advisory-board/advisory-board.component'; export { PreprintsCreatorsFilterComponent } from '@osf/features/preprints/components/filters/preprints-creators-filter/preprints-creators-filter.component'; export { PreprintsDateCreatedFilterComponent } from '@osf/features/preprints/components/filters/preprints-date-created-filter/preprints-date-created-filter.component'; @@ -11,3 +10,6 @@ export { PreprintsFilterChipsComponent } from '@osf/features/preprints/component export { PreprintsHelpDialogComponent } from '@osf/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component'; export { PreprintsResourcesComponent } from '@osf/features/preprints/components/preprints-resources/preprints-resources.component'; export { PreprintsResourcesFiltersComponent } from '@osf/features/preprints/components/preprints-resources-filters/preprints-resources-filters.component'; +export { FileStepComponent } from '@osf/features/preprints/components/submit-steps/file-step/file-step.component'; +export { MetadataComponent } from '@osf/features/preprints/components/submit-steps/metadata/metadata.component'; +export { TitleAndAbstractStepComponent } from '@osf/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component'; diff --git a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html new file mode 100644 index 000000000..944393108 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.html @@ -0,0 +1,32 @@ + +

{{ 'project.overview.metadata.contributors' | translate }}

+
+ + + Warning: Changing your permissions will prevent you from editing your draft. + +
+ +
+ @if (hasChanges) { +
+ + +
+ } + +
+
diff --git a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.scss b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.spec.ts b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.spec.ts new file mode 100644 index 000000000..fb45f2cde --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContributorsComponent } from './contributors.component'; + +describe('ContributorsComponent', () => { + let component: ContributorsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ContributorsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ContributorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts new file mode 100644 index 000000000..9980a2a29 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/metadata/contributors/contributors.component.ts @@ -0,0 +1,191 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { DialogService } from 'primeng/dynamicdialog'; +import { Message } from 'primeng/message'; +import { TableModule } from 'primeng/table'; + +import { filter, forkJoin } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; + +import { + AddContributor, + DeleteContributor, + FetchContributors, + SubmitPreprintSelectors, + UpdateContributor, +} from '@osf/features/preprints/store/submit-preprint'; +import { EducationHistoryDialogComponent, EmploymentHistoryDialogComponent } from '@osf/shared/components'; +import { + AddContributorDialogComponent, + AddUnregisteredContributorDialogComponent, + ContributorsListComponent, +} from '@osf/shared/components/contributors'; +import { AddContributorType } from '@osf/shared/components/contributors/enums'; +import { ContributorDialogAddModel, ContributorModel } from '@osf/shared/components/contributors/models'; +import { CustomConfirmationService, ToastService } from '@osf/shared/services'; +import { findChangedItems } from '@osf/shared/utils'; + +@Component({ + selector: 'osf-preprint-contributors', + imports: [FormsModule, TableModule, ContributorsListComponent, TranslatePipe, Card, Button, Message], + templateUrl: './contributors.component.html', + styleUrl: './contributors.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DialogService], +}) +export class ContributorsComponent implements OnInit { + readonly destroyRef = inject(DestroyRef); + readonly translateService = inject(TranslateService); + readonly dialogService = inject(DialogService); + readonly toastService = inject(ToastService); + readonly customConfirmationService = inject(CustomConfirmationService); + + protected initialContributors = select(SubmitPreprintSelectors.getContributors); + protected contributors = signal([]); + + protected readonly isContributorsLoading = select(SubmitPreprintSelectors.areContributorsLoading); + + protected actions = createDispatchMap({ + getContributors: FetchContributors, + deleteContributor: DeleteContributor, + updateContributor: UpdateContributor, + addContributor: AddContributor, + }); + + get hasChanges(): boolean { + return JSON.stringify(this.initialContributors()) !== JSON.stringify(this.contributors()); + } + + constructor() { + effect(() => { + this.contributors.set(JSON.parse(JSON.stringify(this.initialContributors()))); + }); + } + + ngOnInit(): void { + this.actions.getContributors(); + } + + cancel() { + this.contributors.set(JSON.parse(JSON.stringify(this.initialContributors()))); + } + + save() { + const updatedContributors = findChangedItems(this.initialContributors(), this.contributors(), 'id'); + + const updateRequests = updatedContributors.map((payload) => this.actions.updateContributor(payload)); + + forkJoin(updateRequests).subscribe(() => { + this.toastService.showSuccess('project.contributors.toastMessages.multipleUpdateSuccessMessage'); + }); + } + + openEmploymentHistory(contributor: ContributorModel) { + this.dialogService.open(EmploymentHistoryDialogComponent, { + width: '552px', + data: contributor.employment, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.employment'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + openEducationHistory(contributor: ContributorModel) { + this.dialogService.open(EducationHistoryDialogComponent, { + width: '552px', + data: contributor.education, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.education'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + openAddContributorDialog() { + const addedContributorIds = this.initialContributors().map((x) => x.userId); + + this.dialogService + .open(AddContributorDialogComponent, { + width: '448px', + data: addedContributorIds, + focusOnShow: false, + header: this.translateService.instant('project.contributors.addDialog.addRegisteredContributor'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe( + filter((res: ContributorDialogAddModel) => !!res), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((res: ContributorDialogAddModel) => { + if (res.type === AddContributorType.Unregistered) { + this.openAddUnregisteredContributorDialog(); + } else { + const addRequests = res.data.map((payload) => this.actions.addContributor(payload)); + + forkJoin(addRequests).subscribe(() => { + this.toastService.showSuccess('project.contributors.toastMessages.multipleAddSuccessMessage'); + }); + } + }); + } + + openAddUnregisteredContributorDialog() { + this.dialogService + .open(AddUnregisteredContributorDialogComponent, { + width: '448px', + focusOnShow: false, + header: this.translateService.instant('project.contributors.addDialog.addUnregisteredContributor'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe( + filter((res: ContributorDialogAddModel) => !!res), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((res: ContributorDialogAddModel) => { + if (res.type === AddContributorType.Registered) { + this.openAddContributorDialog(); + } else { + const successMessage = this.translateService.instant('project.contributors.toastMessages.addSuccessMessage'); + const params = { name: res.data[0].fullName }; + + this.actions.addContributor(res.data[0]).subscribe({ + next: () => this.toastService.showSuccess(successMessage, params), + }); + } + }); + } + + removeContributor(contributor: ContributorModel) { + this.customConfirmationService.confirmDelete({ + headerKey: 'project.contributors.removeDialog.title', + messageKey: 'project.contributors.removeDialog.message', + messageParams: { name: contributor.fullName }, + acceptLabelKey: 'common.buttons.remove', + onConfirm: () => { + this.actions + .deleteContributor(contributor.userId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => + this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { + name: contributor.fullName, + }), + }); + }, + }); + } +} diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html new file mode 100644 index 000000000..9499a0588 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html @@ -0,0 +1,8 @@ +

Metadata

+ + + +
+ + +
diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.scss b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.spec.ts b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.spec.ts new file mode 100644 index 000000000..311ac4e9f --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MetadataComponent } from './metadata.component'; + +describe('MetadataComponent', () => { + let component: MetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MetadataComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MetadataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts new file mode 100644 index 000000000..9b77b82f7 --- /dev/null +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts @@ -0,0 +1,14 @@ +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { ContributorsComponent } from '@osf/features/preprints/components/submit-steps/metadata/contributors/contributors.component'; + +@Component({ + selector: 'osf-preprint-metadata', + imports: [ContributorsComponent, Button], + templateUrl: './metadata.component.html', + styleUrl: './metadata.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MetadataComponent {} diff --git a/src/app/features/preprints/constants/preprints.routes.ts b/src/app/features/preprints/constants/preprints.routes.ts index 43fee02e6..3d039e468 100644 --- a/src/app/features/preprints/constants/preprints.routes.ts +++ b/src/app/features/preprints/constants/preprints.routes.ts @@ -8,6 +8,7 @@ import { PreprintsDiscoverState } from '@osf/features/preprints/store/preprints- import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/preprints-resources-filters'; import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; import { SubmitPreprintState } from '@osf/features/preprints/store/submit-preprint'; +import { ContributorsState } from '@shared/components/contributors/store'; export const preprintsRoutes: Routes = [ { @@ -20,6 +21,7 @@ export const preprintsRoutes: Routes = [ PreprintsResourcesFiltersState, PreprintsResourcesFiltersOptionsState, SubmitPreprintState, + ContributorsState, ]), ], children: [ 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 080c96594..a172ab93c 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 @@ -39,6 +39,9 @@

{{ 'Add a ' + preprintProvider()!.preprintWor (backClicked)="currentStep.set(currentStep() - 1)" /> } + @case (2) { + + } @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 9ee1edfb4..72dbca261 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 @@ -17,8 +17,11 @@ import { import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; -import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; -import { FileStepComponent } from '@osf/features/preprints/components/submit-steps/file-step/file-step.component'; +import { + FileStepComponent, + MetadataComponent, + TitleAndAbstractStepComponent, +} from '@osf/features/preprints/components'; import { submitPreprintSteps } from '@osf/features/preprints/constants'; import { BrandService } from '@osf/features/preprints/services'; import { @@ -35,7 +38,7 @@ import { BrowserTabHelper, HeaderStyleHelper, IS_WEB } from '@shared/utils'; @Component({ selector: 'osf-submit-preprint-stepper', - imports: [Skeleton, StepperComponent, TitleAndAbstractStepComponent, FileStepComponent], + imports: [Skeleton, StepperComponent, TitleAndAbstractStepComponent, FileStepComponent, MetadataComponent], templateUrl: './submit-preprint-stepper.component.html', styleUrl: './submit-preprint-stepper.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -58,7 +61,7 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { preprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.providerId())); isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); - currentStep = signal(1); + currentStep = signal(2); isWeb = toSignal(inject(IS_WEB)); constructor() { diff --git a/src/app/features/preprints/services/contributors.service.ts b/src/app/features/preprints/services/contributors.service.ts new file mode 100644 index 000000000..ace44a5f5 --- /dev/null +++ b/src/app/features/preprints/services/contributors.service.ts @@ -0,0 +1,50 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponse } from '@core/models'; +import { JsonApiService } from '@core/services'; +import { AddContributorType } from '@shared/components/contributors/enums'; +import { ContributorsMapper } from '@shared/components/contributors/mappers'; +import { ContributorAddModel, ContributorModel, ContributorResponse } from '@shared/components/contributors/models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class ContributorsService { + private jsonApiService = inject(JsonApiService); + private apiUrl = environment.apiUrl; + + getContributors(preprintId: string): Observable { + return this.jsonApiService + .get>(`${this.apiUrl}/preprints/${preprintId}/contributors/`) + .pipe(map((contributors) => ContributorsMapper.fromResponse(contributors.data))); + } + + addContributor(preprintId: string, data: ContributorAddModel): Observable { + const baseUrl = `${this.apiUrl}/preprints/${preprintId}/contributors/`; + const type = data.id ? AddContributorType.Registered : AddContributorType.Unregistered; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data, type) }; + + return this.jsonApiService + .post(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + } + + updateContributor(preprintId: string, data: ContributorModel): Observable { + const baseUrl = `${environment.apiUrl}/preprints/${preprintId}/contributors/${data.userId}`; + + const contributorData = { data: ContributorsMapper.toContributorAddRequest(data) }; + + return this.jsonApiService + .patch(baseUrl, contributorData) + .pipe(map((contributor) => ContributorsMapper.fromContributorResponse(contributor))); + } + + deleteContributor(preprintId: string, contributorId: string): Observable { + return this.jsonApiService.delete(`${this.apiUrl}/preprints/${preprintId}/contributors/${contributorId}`); + } +} diff --git a/src/app/features/preprints/services/index.ts b/src/app/features/preprints/services/index.ts index d93e065fa..4c00c2c22 100644 --- a/src/app/features/preprints/services/index.ts +++ b/src/app/features/preprints/services/index.ts @@ -1,3 +1,4 @@ export { BrandService } from './brand.service'; +export { ContributorsService } from './contributors.service'; export { PreprintsService } from './preprints.service'; export { PreprintsFiltersOptionsService } from './preprints-resource-filters.service'; diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index ab3585630..a928a177e 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -1,6 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; +import { ContributorAddModel, ContributorModel } from '@shared/components/contributors/models'; import { OsfFile } from '@shared/models'; export class SetSelectedPreprintProviderId { @@ -78,6 +79,28 @@ export class GetProjectFilesByLink { constructor(public filesLink: string) {} } +export class FetchContributors { + static readonly type = '[Submit Preprint] Fetch Contributors'; +} + +export class AddContributor { + static readonly type = '[Submit Preprint] Add Contributor'; + + constructor(public contributor: ContributorAddModel) {} +} + +export class UpdateContributor { + static readonly type = '[Submit Preprint] Update Contributor'; + + constructor(public contributor: ContributorModel) {} +} + +export class DeleteContributor { + static readonly type = '[Submit Preprint] Delete Contributor'; + + constructor(public userId: string) {} +} + export class ResetStateAndDeletePreprint { static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index f26210c44..1a08575a5 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -1,6 +1,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint, PreprintFilesLinks } from '@osf/features/preprints/models'; +import { ContributorModel } from '@shared/components/contributors/models'; import { AsyncStateModel, IdName, OsfFile } from '@shared/models'; export interface SubmitPreprintStateModel { @@ -11,4 +12,5 @@ export interface SubmitPreprintStateModel { preprintFiles: AsyncStateModel; availableProjects: AsyncStateModel; projectFiles: AsyncStateModel; + contributors: AsyncStateModel; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index 9267ceb23..ed3836b28 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -57,4 +57,14 @@ export class SubmitPreprintSelectors { static areProjectFilesLoading(state: SubmitPreprintStateModel) { return state.projectFiles.isLoading; } + + @Selector([SubmitPreprintState]) + static getContributors(state: SubmitPreprintStateModel) { + return state.contributors.data; + } + + @Selector([SubmitPreprintState]) + static areContributorsLoading(state: SubmitPreprintStateModel) { + return state.contributors.isLoading; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 2f2c6b4cf..cc7f994f5 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -1,5 +1,5 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { patch } from '@ngxs/store/operators'; +import { insertItem, patch, removeItem, updateItem } from '@ngxs/store/operators'; import { EMPTY, filter, switchMap, tap, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -9,13 +9,16 @@ import { inject, Injectable } from '@angular/core'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; -import { PreprintsService } from '@osf/features/preprints/services'; +import { ContributorsService, PreprintsService } from '@osf/features/preprints/services'; import { OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; import { + AddContributor, CopyFileFromProject, CreatePreprint, + DeleteContributor, + FetchContributors, GetAvailableProjects, GetPreprintFiles, GetPreprintFilesLinks, @@ -26,6 +29,7 @@ import { SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprintStateModel, + UpdateContributor, UpdatePreprint, UploadFile, } from './'; @@ -35,7 +39,7 @@ import { defaults: { selectedProviderId: null, createdPreprint: { - data: null, + data: { id: '6s4jg_v1' } as Preprint, // Temporary default value for testing isLoading: false, error: null, isSubmitting: false, @@ -61,12 +65,18 @@ import { isLoading: false, error: null, }, + contributors: { + data: [], + isLoading: false, + error: null, + }, }, }) @Injectable() export class SubmitPreprintState { private preprintsService = inject(PreprintsService); private fileService = inject(FilesService); + private contributorsService = inject(ContributorsService); @Action(SetSelectedPreprintProviderId) setSelectedPreprintProviderId(ctx: StateContext, action: SetSelectedPreprintProviderId) { @@ -286,6 +296,11 @@ export class SubmitPreprintState { isLoading: false, error: null, }, + contributors: { + data: [], + isLoading: false, + error: null, + }, }); if (createdPreprintId) { return this.preprintsService.deletePreprint(createdPreprintId); @@ -336,6 +351,88 @@ export class SubmitPreprintState { ); } + @Action(FetchContributors) + fetchContributors(ctx: StateContext) { + const createdPreprint = ctx.getState().createdPreprint.data; + if (!createdPreprint) { + return; + } + + ctx.setState(patch({ contributors: patch({ isLoading: true }) })); + + return this.contributorsService.getContributors(createdPreprint.id).pipe( + tap((contributors) => { + ctx.setState(patch({ contributors: patch({ isLoading: false, data: contributors }) })); + }), + catchError((error) => this.handleError(ctx, 'contributors', error)) + ); + } + + @Action(AddContributor) + addContributor(ctx: StateContext, action: AddContributor) { + const createdPreprint = ctx.getState().createdPreprint.data; + if (!createdPreprint) { + return; + } + + ctx.setState(patch({ contributors: patch({ isLoading: true }) })); + + return this.contributorsService.addContributor(createdPreprint.id, action.contributor).pipe( + tap((contributor) => { + ctx.setState(patch({ contributors: patch({ isLoading: false, data: insertItem(contributor) }) })); + }), + catchError((error) => this.handleError(ctx, 'contributors', error)) + ); + } + + @Action(UpdateContributor) + updateContributor(ctx: StateContext, action: UpdateContributor) { + const createdPreprint = ctx.getState().createdPreprint.data; + if (!createdPreprint) { + return; + } + + ctx.setState(patch({ contributors: patch({ isLoading: true }) })); + + return this.contributorsService.updateContributor(createdPreprint.id, action.contributor).pipe( + tap((contributor) => { + ctx.setState( + patch({ + contributors: patch({ + isLoading: false, + data: updateItem((item) => item.id === action.contributor.id, contributor), + }), + }) + ); + }), + catchError((error) => this.handleError(ctx, 'contributors', error)) + ); + } + + @Action(DeleteContributor) + deleteContributor(ctx: StateContext, action: DeleteContributor) { + const createdPreprint = ctx.getState().createdPreprint.data; + if (!createdPreprint) { + return; + } + + ctx.setState(patch({ contributors: patch({ isLoading: true }) })); + + return this.contributorsService.deleteContributor(createdPreprint.id, action.userId).pipe( + tap(() => { + ctx.setState( + patch({ + contributors: patch({ + isLoading: false, + data: removeItem((item) => action.userId === item.userId), + }), + }) + ); + }), + catchError((error) => this.handleError(ctx, 'contributors', error)) + ); + } + private handleError( ctx: StateContext, section: keyof SubmitPreprintStateModel, diff --git a/src/assets/styles/overrides/message.scss b/src/assets/styles/overrides/message.scss index 059d2a5dd..09e3d1e0f 100644 --- a/src/assets/styles/overrides/message.scss +++ b/src/assets/styles/overrides/message.scss @@ -1,4 +1,5 @@ -@use "../variables" as var; +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; .p-message:not(.p-message-simple) { border: none; @@ -19,16 +20,17 @@ } .warning-message { + --p-message-text-font-weight: 700; + --p-message-text-font-size: mix.rem(14px); + .p-message { width: fit-content; height: fit-content; color: var.$red-1; background: var.$red-2; padding: 1rem; - - .p-message-text { - font-weight: 700; - } + border: none; + outline: none; } } From 2bd0ca048231656b162c1c856411ec2d156fe469 Mon Sep 17 00:00:00 2001 From: Roma Date: Fri, 27 Jun 2025 21:03:58 +0300 Subject: [PATCH 28/35] style(base): Removed existing file --- src/assets/styles/_base.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/assets/styles/_base.scss b/src/assets/styles/_base.scss index 6e15fe164..9f657d0ab 100644 --- a/src/assets/styles/_base.scss +++ b/src/assets/styles/_base.scss @@ -93,7 +93,3 @@ font-weight: 400; } } - -.cursor-not-allowed { - cursor: not-allowed; -} From 9be74a600438463e9fac6067fceea876b47b5b3c Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 30 Jun 2025 15:12:57 +0300 Subject: [PATCH 29/35] feat(submit-preprint-metadata-step): Implemented doi, publication date and citation sections --- .../metadata/metadata.component.html | 57 +++++++++++- .../metadata/metadata.component.scss | 7 ++ .../metadata/metadata.component.ts | 90 ++++++++++++++++++- .../title-and-abstract-step.component.ts | 1 - .../constants/form-input-limits.const.ts | 6 ++ .../preprints/mappers/preprints.mapper.ts | 3 + .../preprints/models/preprints.models.ts | 3 + .../models/submit-preprint-form.models.ts | 8 ++ .../submit-preprint-stepper.component.html | 2 +- .../preprints/services/preprints.service.ts | 3 + src/app/shared/utils/find-changed-fields.ts | 27 ++++++ src/app/shared/utils/index.ts | 1 + src/assets/styles/overrides/date-picker.scss | 7 ++ src/assets/styles/styles.scss | 1 + 14 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 src/app/shared/utils/find-changed-fields.ts create mode 100644 src/assets/styles/overrides/date-picker.scss diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html index 9499a0588..d5af72d47 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html @@ -2,7 +2,62 @@

Metadata

+ +
+

Publication DOI

+ + + @let doiControl = metadataForm.controls['doi']; + @if (doiControl.errors?.['required'] && (doiControl.touched || doiControl.dirty)) { + + {{ INPUT_VALIDATION_MESSAGES.required | translate }} + + } + @if (doiControl.errors?.['pattern'] && (doiControl.touched || doiControl.dirty)) { + Please use a valid DOI format (10.xxxx/xxxxx) + + } +
+
+ + +

Publication Date (optional)

+ + + + + + +
+ + +

Publication Citation (optional)

+ + +
+
- +
diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.scss b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.scss index e69de29bb..243cc50eb 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.scss +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.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/submit-steps/metadata/metadata.component.ts b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts index 9b77b82f7..864e5a560 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts @@ -1,14 +1,98 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { DatePicker } from 'primeng/datepicker'; +import { InputText } from 'primeng/inputtext'; +import { Message } from 'primeng/message'; +import { Tooltip } from 'primeng/tooltip'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostListener, OnInit, output } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { ContributorsComponent } from '@osf/features/preprints/components/submit-steps/metadata/contributors/contributors.component'; +import { formInputLimits } from '@osf/features/preprints/constants'; +import { MetadataForm, Preprint } from '@osf/features/preprints/models'; +import { CreatePreprint, SubmitPreprintSelectors, UpdatePreprint } from '@osf/features/preprints/store/submit-preprint'; +import { IconComponent, TextInputComponent } from '@shared/components'; +import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; +import { CustomValidators, findChangedFields } from '@shared/utils'; @Component({ selector: 'osf-preprint-metadata', - imports: [ContributorsComponent, Button], + imports: [ + ContributorsComponent, + Button, + Card, + ReactiveFormsModule, + Message, + TranslatePipe, + DatePicker, + IconComponent, + InputText, + TextInputComponent, + Tooltip, + ], templateUrl: './metadata.component.html', styleUrl: './metadata.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MetadataComponent {} +export class MetadataComponent implements OnInit { + private actions = createDispatchMap({ + createPreprint: CreatePreprint, + updatePreprint: UpdatePreprint, + }); + + protected metadataForm!: FormGroup; + protected inputLimits = formInputLimits; + protected readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; + + createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); + nextClicked = output(); + + ngOnInit() { + this.initForm(); + } + + initForm() { + const publicationDate = this.createdPreprint()?.originalPublicationDate; + this.metadataForm = new FormGroup({ + doi: new FormControl(this.createdPreprint()?.doi || '', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed(), Validators.pattern(this.inputLimits.doi.pattern)], + }), + originalPublicationDate: new FormControl(publicationDate ? new Date(publicationDate) : null, { + nonNullable: false, + validators: [], + }), + customPublicationCitation: new FormControl(this.createdPreprint()?.customPublicationCitation || null, { + nonNullable: false, + validators: [Validators.maxLength(this.inputLimits.citation.maxLength)], + }), + }); + } + + nextButtonClicked() { + if (this.metadataForm.invalid) { + return; + } + + const model = this.metadataForm.value; + + const changedFields = findChangedFields(model, this.createdPreprint()!); + + this.actions.updatePreprint(this.createdPreprint()!.id, changedFields).subscribe({ + complete: () => { + this.nextClicked.emit(); + }, + }); + } + + @HostListener('window:beforeunload', ['$event']) + public onBeforeUnload($event: BeforeUnloadEvent): boolean { + $event.preventDefault(); + return false; + } +} diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts index 8941ea0ca..ad43bf852 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.ts @@ -53,7 +53,6 @@ export class TitleAndAbstractStepComponent implements OnInit { isUpdatingPreprint = select(SubmitPreprintSelectors.isPreprintSubmitting); nextClicked = output(); - //[RNi] TODO: Handle back button click ngOnInit() { this.initForm(); } diff --git a/src/app/features/preprints/constants/form-input-limits.const.ts b/src/app/features/preprints/constants/form-input-limits.const.ts index 23d991b1e..0a72fe7d4 100644 --- a/src/app/features/preprints/constants/form-input-limits.const.ts +++ b/src/app/features/preprints/constants/form-input-limits.const.ts @@ -6,4 +6,10 @@ export const formInputLimits = { title: { maxLength: 100, }, + doi: { + pattern: /^10\.\d{4}\/[\s\S]{2,500}$/, + }, + citation: { + maxLength: 500, + }, }; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index a30b1fd87..639054c87 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -85,6 +85,9 @@ export class PreprintsMapper { dateModified: response.attributes.date_modified, title: response.attributes.title, description: response.attributes.description, + doi: response.attributes.doi, + customPublicationCitation: response.attributes.custom_publication_citation, + originalPublicationDate: response.attributes.original_publication_date, isPublished: response.attributes.is_published, tags: response.attributes.tags, isPublic: response.attributes.public, diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 6d3a1e76f..50f4bec3c 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -47,6 +47,9 @@ export interface Preprint { dateModified: string; title: string; description: string; + doi: StringOrNull; + originalPublicationDate: Date | null; + customPublicationCitation: StringOrNull; isPublished: boolean; tags: string[]; isPublic: boolean; diff --git a/src/app/features/preprints/models/submit-preprint-form.models.ts b/src/app/features/preprints/models/submit-preprint-form.models.ts index 2eefdc073..4c2a128db 100644 --- a/src/app/features/preprints/models/submit-preprint-form.models.ts +++ b/src/app/features/preprints/models/submit-preprint-form.models.ts @@ -1,6 +1,14 @@ import { FormControl } from '@angular/forms'; +import { StringOrNull } from '@core/helpers'; + export interface TitleAndAbstractForm { title: FormControl; description: FormControl; } + +export interface MetadataForm { + doi: FormControl; + originalPublicationDate: FormControl; + customPublicationCitation: FormControl; +} 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 a172ab93c..93c84fd36 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 @@ -40,7 +40,7 @@

{{ 'Add a ' + preprintProvider()!.preprintWor /> } @case (2) { - + } @default {

No such step

diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 117028d1f..a5e146bd0 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -33,6 +33,9 @@ export class PreprintsService { private domainToApiFieldMap: Record = { title: 'title', description: 'description', + originalPublicationDate: 'original_publication_date', + doi: 'doi', + customPublicationCitation: 'custom_publication_citation', }; getPreprintProviderById(id: string): Observable { diff --git a/src/app/shared/utils/find-changed-fields.ts b/src/app/shared/utils/find-changed-fields.ts new file mode 100644 index 000000000..d023e253d --- /dev/null +++ b/src/app/shared/utils/find-changed-fields.ts @@ -0,0 +1,27 @@ +/** + * Compares a partial form model with a full object and returns only the fields that have changed. + * Useful for detecting which fields in a form differ from the original object state. + * + * Uses `JSON.stringify` for deep equality comparison and handles basic types, arrays, and objects. + * Note: Differences in date formatting or time zones (e.g., missing 'Z' in ISO strings) may cause false positives. + * + * @param formModel - A partial object representing the edited form values. + * @param currentObject - The original full object to compare against. + * @returns A partial object containing only the fields from `formModel` that differ from `currentObject`. + */ +export function findChangedFields(formModel: Partial, currentObject: T): Partial { + const result: Partial = {}; + + for (const key of Object.keys(formModel) as (keyof T)[]) { + const formVal = formModel[key]; + const currentVal = currentObject[key]; + + const isEqual = JSON.stringify(formVal) === JSON.stringify(currentVal); + + if (!isEqual) { + result[key] = formVal; + } + } + + return result; +} diff --git a/src/app/shared/utils/index.ts b/src/app/shared/utils/index.ts index 204b4de20..77bce136c 100644 --- a/src/app/shared/utils/index.ts +++ b/src/app/shared/utils/index.ts @@ -5,6 +5,7 @@ export * from './breakpoints.tokens'; export { BrowserTabHelper } from './browser-tab.helper'; export * from './custom-form-validators.helper'; export * from './default-confirmation-config.helper'; +export * from './find-changed-fields'; export * from './find-changed-items.helper'; export * from './get-resource-types.helper'; export * from './pie-chart-palette'; diff --git a/src/assets/styles/overrides/date-picker.scss b/src/assets/styles/overrides/date-picker.scss new file mode 100644 index 000000000..26196c4aa --- /dev/null +++ b/src/assets/styles/overrides/date-picker.scss @@ -0,0 +1,7 @@ +@use "assets/styles/mixins" as mix; + +.half-width-datepicker { + .p-datepicker-panel { + min-width: mix.rem(350px) !important; + } +} diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 54217549e..84b2edaa3 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -43,3 +43,4 @@ @use "./components/md-editor"; @use "./components/preprints"; @use "./overrides/cedar-metadata"; +@use "./overrides/date-picker"; From cfcdc84b5db6b6ccd6e7ce1ce197733f5242f0d2 Mon Sep 17 00:00:00 2001 From: Roma Date: Mon, 30 Jun 2025 15:15:35 +0300 Subject: [PATCH 30/35] fix(title-and-abstract): Fixed PR comments --- .../title-and-abstract-step.component.html | 6 +++++- .../title-and-abstract-step.component.scss | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html index 050d49ed8..9a8c1520c 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.html @@ -3,7 +3,11 @@

Title and Abstract

- +
diff --git a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss index 18b6612e7..243cc50eb 100644 --- a/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss +++ b/src/app/features/preprints/components/submit-steps/title-and-abstract-step/title-and-abstract-step.component.scss @@ -1,4 +1,3 @@ -@use "assets/styles/mixins" as mix; @use "assets/styles/variables" as var; .card { From 7d8852efbcd90af77a8be29ccbdeab6178f189b6 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 1 Jul 2025 12:16:18 +0300 Subject: [PATCH 31/35] feat(licenses): Introduced shared component and models. Used in metadata step --- src/app/core/services/index.ts | 2 +- src/app/core/services/licenses.service.ts | 20 ---- .../metadata/metadata.component.html | 16 ++- .../metadata/metadata.component.ts | 24 +++- .../preprints/mappers/preprints.mapper.ts | 7 ++ src/app/features/preprints/models/index.ts | 1 + .../preprint-licenses-json-api.models.ts | 21 ++++ .../preprints/models/preprints.models.ts | 11 +- .../submit-preprint-stepper.component.ts | 7 +- .../preprints/services/licenses.service.ts | 64 ++++++++++ .../submit-preprint.actions.ts | 15 ++- .../submit-preprint/submit-preprint.model.ts | 2 + .../submit-preprint.selectors.ts | 5 + .../submit-preprint/submit-preprint.state.ts | 41 +++++++ .../license-dialog.component.ts | 4 +- src/app/shared/components/index.ts | 1 + .../components/license/license.component.html | 67 +++++++++++ .../components/license/license.component.scss | 4 + .../license/license.component.spec.ts | 22 ++++ .../components/license/license.component.ts | 111 ++++++++++++++++++ .../shared/components/license/models/index.ts | 1 + .../license/models/license-form.models.ts | 6 + src/app/shared/mappers/index.ts | 1 + src/app/shared/mappers/licenses.mapper.ts | 14 +++ src/app/shared/models/index.ts | 2 + src/app/shared/models/license.model.ts | 36 ++---- .../shared/models/licenses-json-api.model.ts | 21 ++++ src/app/shared/pipes/index.ts | 1 + src/app/shared/pipes/interpolate.pipe.ts | 10 ++ src/app/shared/services/index.ts | 1 + src/app/shared/services/licenses.service.ts | 23 ++++ .../shared/stores/licenses/licenses.state.ts | 6 +- src/assets/i18n/en.json | 14 ++- 33 files changed, 513 insertions(+), 68 deletions(-) delete mode 100644 src/app/core/services/licenses.service.ts create mode 100644 src/app/features/preprints/models/preprint-licenses-json-api.models.ts create mode 100644 src/app/features/preprints/services/licenses.service.ts create mode 100644 src/app/shared/components/license/license.component.html create mode 100644 src/app/shared/components/license/license.component.scss create mode 100644 src/app/shared/components/license/license.component.spec.ts create mode 100644 src/app/shared/components/license/license.component.ts create mode 100644 src/app/shared/components/license/models/index.ts create mode 100644 src/app/shared/components/license/models/license-form.models.ts create mode 100644 src/app/shared/mappers/licenses.mapper.ts create mode 100644 src/app/shared/models/licenses-json-api.model.ts create mode 100644 src/app/shared/pipes/interpolate.pipe.ts create mode 100644 src/app/shared/services/licenses.service.ts diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index 6590aa1cc..55d64238d 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -1,4 +1,4 @@ export { JsonApiService } from './json-api.service'; -export { LicensesService } from './licenses.service'; export { RequestAccessService } from './request-access.service'; export { UserService } from './user.service'; +export { LicensesService } from '@shared/services/licenses.service'; diff --git a/src/app/core/services/licenses.service.ts b/src/app/core/services/licenses.service.ts deleted file mode 100644 index e7f206b6f..000000000 --- a/src/app/core/services/licenses.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Observable } from 'rxjs'; - -import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; - -import { LicensesResponseJsonApi } from '@shared/models/license.model'; - -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class LicensesService { - private readonly http = inject(HttpClient); - private readonly baseUrl = environment.apiUrl; - - getAllLicenses(): Observable { - return this.http.get(`${this.baseUrl}/licenses/?page[size]=20`); - } -} diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html index d5af72d47..9c8bc9aac 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html @@ -1,6 +1,18 @@ -

Metadata

+

Metadata

- +
+ +
+ +
+ +
diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts index 864e5a560..33da63197 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts @@ -15,9 +15,16 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula import { ContributorsComponent } from '@osf/features/preprints/components/submit-steps/metadata/contributors/contributors.component'; import { formInputLimits } from '@osf/features/preprints/constants'; import { MetadataForm, Preprint } from '@osf/features/preprints/models'; -import { CreatePreprint, SubmitPreprintSelectors, UpdatePreprint } from '@osf/features/preprints/store/submit-preprint'; -import { IconComponent, TextInputComponent } from '@shared/components'; +import { + CreatePreprint, + FetchLicenses, + SaveLicense, + SubmitPreprintSelectors, + UpdatePreprint, +} from '@osf/features/preprints/store/submit-preprint'; +import { IconComponent, LicenseComponent, TextInputComponent } from '@shared/components'; import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; +import { License, LicenseOptions } from '@shared/models'; import { CustomValidators, findChangedFields } from '@shared/utils'; @Component({ @@ -34,6 +41,7 @@ import { CustomValidators, findChangedFields } from '@shared/utils'; InputText, TextInputComponent, Tooltip, + LicenseComponent, ], templateUrl: './metadata.component.html', styleUrl: './metadata.component.scss', @@ -43,16 +51,20 @@ export class MetadataComponent implements OnInit { private actions = createDispatchMap({ createPreprint: CreatePreprint, updatePreprint: UpdatePreprint, + fetchLicenses: FetchLicenses, + saveLicense: SaveLicense, }); protected metadataForm!: FormGroup; protected inputLimits = formInputLimits; protected readonly INPUT_VALIDATION_MESSAGES = INPUT_VALIDATION_MESSAGES; + licences = select(SubmitPreprintSelectors.getLicenses); createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); nextClicked = output(); ngOnInit() { + this.actions.fetchLicenses(); this.initForm(); } @@ -95,4 +107,12 @@ export class MetadataComponent implements OnInit { $event.preventDefault(); return false; } + + createLicense(licenseDetails: { id: string; licenseOptions: LicenseOptions }) { + this.actions.saveLicense(licenseDetails.id, licenseDetails.licenseOptions); + } + + selectLicense(license: License) { + this.actions.saveLicense(license.id); + } } diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index 639054c87..e16bbfa22 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -94,6 +94,13 @@ export class PreprintsMapper { version: response.attributes.version, isLatestVersion: response.attributes.is_latest_version, primaryFileId: response.relationships.primary_file?.links?.related?.href || null, + licenseId: response.relationships.license?.data?.id || null, + licenseOptions: response.attributes.license_record + ? { + year: response.attributes.license_record.year, + copyrightHolder: response.attributes.license_record.copyright_holders[0], + } + : null, }; } } diff --git a/src/app/features/preprints/models/index.ts b/src/app/features/preprints/models/index.ts index 8c262520f..1cf98e527 100644 --- a/src/app/features/preprints/models/index.ts +++ b/src/app/features/preprints/models/index.ts @@ -1,2 +1,3 @@ +export * from './preprint-licenses-json-api.models'; export * from './preprints.models'; export * from './submit-preprint-form.models'; diff --git a/src/app/features/preprints/models/preprint-licenses-json-api.models.ts b/src/app/features/preprints/models/preprint-licenses-json-api.models.ts new file mode 100644 index 000000000..5d5efeea2 --- /dev/null +++ b/src/app/features/preprints/models/preprint-licenses-json-api.models.ts @@ -0,0 +1,21 @@ +import { LicenseRecordJsonApi } from '@shared/models'; + +export interface PreprintLicenseRelationshipJsonApi { + id: string; + type: 'licenses'; +} + +export interface PreprintLicensePayloadJsonApi { + data: { + type: 'preprints'; + id: string; + relationships: { + license: { + data: PreprintLicenseRelationshipJsonApi; + }; + }; + attributes: { + license_record?: LicenseRecordJsonApi; + }; + }; +} diff --git a/src/app/features/preprints/models/preprints.models.ts b/src/app/features/preprints/models/preprints.models.ts index 50f4bec3c..26852812b 100644 --- a/src/app/features/preprints/models/preprints.models.ts +++ b/src/app/features/preprints/models/preprints.models.ts @@ -1,4 +1,5 @@ import { StringOrNull } from '@core/helpers'; +import { LicenseOptions, LicenseRecordJsonApi } from '@shared/models'; export interface Brand { id: string; @@ -56,6 +57,8 @@ export interface Preprint { version: number; isLatestVersion: boolean; primaryFileId: StringOrNull; + licenseId: StringOrNull; + licenseOptions: LicenseOptions | null; } export interface PreprintFilesLinks { @@ -125,7 +128,7 @@ export interface PreprintJsonApi { description: string; is_published: boolean; is_preprint_orphan: boolean; - license_record: StringOrNull; + license_record: LicenseRecordJsonApi | null; tags: string[]; date_withdrawn: Date | null; current_user_permissions: string[]; @@ -147,4 +150,10 @@ export interface PreprintsRelationshipsJsonApi { }; }; }; + license: { + data: { + id: string; + type: 'licenses'; + }; + }; } 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 72dbca261..31521338e 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 @@ -24,11 +24,7 @@ import { } from '@osf/features/preprints/components'; import { submitPreprintSteps } from '@osf/features/preprints/constants'; import { BrandService } from '@osf/features/preprints/services'; -import { - GetHighlightedSubjectsByProviderId, - GetPreprintProviderById, - PreprintsSelectors, -} from '@osf/features/preprints/store/preprints'; +import { GetPreprintProviderById, PreprintsSelectors } from '@osf/features/preprints/store/preprints'; import { ResetStateAndDeletePreprint, SetSelectedPreprintProviderId, @@ -52,7 +48,6 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { private actions = createDispatchMap({ getPreprintProviderById: GetPreprintProviderById, - getHighlightedSubjectsByProviderId: GetHighlightedSubjectsByProviderId, setSelectedPreprintProviderId: SetSelectedPreprintProviderId, resetStateAndDeletePreprint: ResetStateAndDeletePreprint, }); diff --git a/src/app/features/preprints/services/licenses.service.ts b/src/app/features/preprints/services/licenses.service.ts new file mode 100644 index 000000000..5238a07ff --- /dev/null +++ b/src/app/features/preprints/services/licenses.service.ts @@ -0,0 +1,64 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { ApiData } from '@core/models'; +import { JsonApiService } from '@core/services'; +import { PreprintsMapper } from '@osf/features/preprints/mappers'; +import { + PreprintJsonApi, + PreprintLicensePayloadJsonApi, + PreprintsRelationshipsJsonApi, +} from '@osf/features/preprints/models'; +import { LicensesMapper } from '@shared/mappers'; +import { License, LicenseOptions, LicensesResponseJsonApi } from '@shared/models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class LicensesService { + private apiUrl = environment.apiUrl; + private readonly jsonApiService = inject(JsonApiService); + + getLicenses(providerId: string): Observable { + return this.jsonApiService + .get(`${this.apiUrl}/providers/preprints/${providerId}/licenses/`, { + 'page[size]': 100, + sort: 'name', + }) + .pipe(map((licenses) => LicensesMapper.fromLicensesResponse(licenses))); + } + + updatePreprintLicense(preprintId: string, licenseId: string, licenseOptions?: LicenseOptions) { + const payload: PreprintLicensePayloadJsonApi = { + data: { + type: 'preprints', + id: preprintId, + relationships: { + license: { + data: { + id: licenseId, + type: 'licenses', + }, + }, + }, + attributes: { + ...(licenseOptions && { + license_record: { + copyright_holders: [licenseOptions.copyrightHolder], + year: licenseOptions.year, + }, + }), + }, + }, + }; + + return this.jsonApiService + .patch< + ApiData + >(`${this.apiUrl}/preprints/${preprintId}/`, payload) + .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); + } +} diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts index a928a177e..9c3b26913 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.actions.ts @@ -2,7 +2,7 @@ import { StringOrNull } from '@core/helpers'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; import { ContributorAddModel, ContributorModel } from '@shared/components/contributors/models'; -import { OsfFile } from '@shared/models'; +import { LicenseOptions, OsfFile } from '@shared/models'; export class SetSelectedPreprintProviderId { static readonly type = '[Submit Preprint] Set Selected Preprint Provider Id'; @@ -101,6 +101,19 @@ export class DeleteContributor { constructor(public userId: string) {} } +export class FetchLicenses { + static readonly type = '[Submit Preprint] Fetch Licenses'; +} + +export class SaveLicense { + static readonly type = '[Submit Preprint] Save License'; + + constructor( + public licenseId: string, + public licenseOptions?: LicenseOptions + ) {} +} + export class ResetStateAndDeletePreprint { static readonly type = '[Submit Preprint] Reset State And Delete Preprint'; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts index 1a08575a5..8a3ea7575 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.model.ts @@ -3,6 +3,7 @@ import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint, PreprintFilesLinks } from '@osf/features/preprints/models'; import { ContributorModel } from '@shared/components/contributors/models'; import { AsyncStateModel, IdName, OsfFile } from '@shared/models'; +import { License } from '@shared/models/license.model'; export interface SubmitPreprintStateModel { selectedProviderId: StringOrNull; @@ -13,4 +14,5 @@ export interface SubmitPreprintStateModel { availableProjects: AsyncStateModel; projectFiles: AsyncStateModel; contributors: AsyncStateModel; + licenses: AsyncStateModel; } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts index ed3836b28..5eb2a2d39 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.selectors.ts @@ -67,4 +67,9 @@ export class SubmitPreprintSelectors { static areContributorsLoading(state: SubmitPreprintStateModel) { return state.contributors.isLoading; } + + @Selector([SubmitPreprintState]) + static getLicenses(state: SubmitPreprintStateModel) { + return state.licenses.data; + } } diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index cc7f994f5..7addc2a61 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -10,6 +10,7 @@ import { inject, Injectable } from '@angular/core'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { Preprint } from '@osf/features/preprints/models'; import { ContributorsService, PreprintsService } from '@osf/features/preprints/services'; +import { LicensesService } from '@osf/features/preprints/services/licenses.service'; import { OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; @@ -19,6 +20,7 @@ import { CreatePreprint, DeleteContributor, FetchContributors, + FetchLicenses, GetAvailableProjects, GetPreprintFiles, GetPreprintFilesLinks, @@ -26,6 +28,7 @@ import { GetProjectFilesByLink, ResetStateAndDeletePreprint, ReuploadFile, + SaveLicense, SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprintStateModel, @@ -70,6 +73,11 @@ import { isLoading: false, error: null, }, + licenses: { + data: [], + isLoading: false, + error: null, + }, }, }) @Injectable() @@ -77,6 +85,7 @@ export class SubmitPreprintState { private preprintsService = inject(PreprintsService); private fileService = inject(FilesService); private contributorsService = inject(ContributorsService); + private licensesService = inject(LicensesService); @Action(SetSelectedPreprintProviderId) setSelectedPreprintProviderId(ctx: StateContext, action: SetSelectedPreprintProviderId) { @@ -301,6 +310,11 @@ export class SubmitPreprintState { isLoading: false, error: null, }, + licenses: { + data: [], + isLoading: false, + error: null, + }, }); if (createdPreprintId) { return this.preprintsService.deletePreprint(createdPreprintId); @@ -433,6 +447,33 @@ export class SubmitPreprintState { ); } + @Action(FetchLicenses) + fetchLicenses(ctx: StateContext) { + const providerId = ctx.getState().selectedProviderId; + if (!providerId) return; + ctx.setState(patch({ licenses: patch({ isLoading: true }) })); + + return this.licensesService.getLicenses(providerId).pipe( + tap((licenses) => { + ctx.setState(patch({ licenses: patch({ isLoading: false, data: licenses }) })); + }), + catchError((error) => this.handleError(ctx, 'licenses', error)) + ); + } + + @Action(SaveLicense) + saveLicense(ctx: StateContext, action: SaveLicense) { + const createdPreprintId = ctx.getState().createdPreprint.data!.id; + ctx.setState(patch({ createdPreprint: patch({ isSubmitting: true }) })); + + return this.licensesService.updatePreprintLicense(createdPreprintId, action.licenseId, action.licenseOptions).pipe( + tap((preprint) => { + ctx.setState(patch({ createdPreprint: patch({ isSubmitting: false, data: preprint }) })); + }), + catchError((error) => this.handleError(ctx, 'createdPreprint', error)) + ); + } + private handleError( ctx: StateContext, section: keyof SubmitPreprintStateModel, diff --git a/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.ts b/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.ts index ba6f220f6..c939898fe 100644 --- a/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.ts +++ b/src/app/features/project/metadata/dialogs/license-dialog/license-dialog.component.ts @@ -52,7 +52,7 @@ export class LicenseDialogComponent implements OnInit { if (currentLicenses) { this.licenseOptions = currentLicenses.map((license: License) => ({ - label: license.attributes.name, + label: license.name, value: license.id, })); } @@ -77,7 +77,7 @@ export class LicenseDialogComponent implements OnInit { const selectedLicense = this.licenses().find((license: License) => license.id === licenseId); if (selectedLicense) { - this.selectedLicenseText.set(selectedLicense.attributes.text); + this.selectedLicenseText.set(selectedLicense.text); } else { this.selectedLicenseText.set(''); } diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 0ef79a98b..4609a2226 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -12,6 +12,7 @@ export { FormSelectComponent } from './form-select/form-select.component'; export { FullScreenLoaderComponent } from './full-screen-loader/full-screen-loader.component'; export { IconComponent } from './icon/icon.component'; export { InfoIconComponent } from './info-icon/info-icon.component'; +export { LicenseComponent } from './license/license.component'; export { LineChartComponent } from './line-chart/line-chart.component'; export { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; export { MarkdownComponent } from './markdown/markdown.component'; diff --git a/src/app/shared/components/license/license.component.html b/src/app/shared/components/license/license.component.html new file mode 100644 index 000000000..8d03eca44 --- /dev/null +++ b/src/app/shared/components/license/license.component.html @@ -0,0 +1,67 @@ + +

{{ 'shared.license.title' | translate }}

+ +

{{ 'shared.license.description' | translate }}

+

+ {{ 'shared.license.helpText' | translate }} + {{ 'common.links.helpGuide' | translate }}. +

+ + + @if (selectedLicense()) { + + @if (selectedLicense()!.requiredFields.length) { +
+
+ + +
+ + + } + +

+ +

+ + @if (selectedLicense()!.requiredFields.length) { +
+ + +
+ } + } +
diff --git a/src/app/shared/components/license/license.component.scss b/src/app/shared/components/license/license.component.scss new file mode 100644 index 000000000..7f863186d --- /dev/null +++ b/src/app/shared/components/license/license.component.scss @@ -0,0 +1,4 @@ +.highlight-block { + padding: 0.5rem; + background-color: var(--bg-blue-2); +} diff --git a/src/app/shared/components/license/license.component.spec.ts b/src/app/shared/components/license/license.component.spec.ts new file mode 100644 index 000000000..58410bcd4 --- /dev/null +++ b/src/app/shared/components/license/license.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LicenseComponent } from '@shared/components'; + +describe('LicenseComponent', () => { + let component: LicenseComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LicenseComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LicenseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/license/license.component.ts b/src/app/shared/components/license/license.component.ts new file mode 100644 index 000000000..cee3225c9 --- /dev/null +++ b/src/app/shared/components/license/license.component.ts @@ -0,0 +1,111 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; +import { DatePicker } from 'primeng/datepicker'; +import { Divider } from 'primeng/divider'; +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, effect, input, model, output } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { StringOrNullOrUndefined } from '@core/helpers'; +import { TextInputComponent, TruncatedTextComponent } from '@shared/components'; +import { LicenseForm } from '@shared/components/license/models'; +import { InputLimits } from '@shared/constants'; +import { License, LicenseOptions } from '@shared/models'; +import { InterpolatePipe } from '@shared/pipes'; +import { CustomValidators } from '@shared/utils'; + +@Component({ + selector: 'osf-license', + imports: [ + Card, + TranslatePipe, + Select, + FormsModule, + Divider, + TruncatedTextComponent, + DatePicker, + TextInputComponent, + ReactiveFormsModule, + Button, + InterpolatePipe, + ], + templateUrl: './license.component.html', + styleUrl: './license.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LicenseComponent { + selectedLicenseId = input(null); + selectedLicenseOptions = input(null); + licenses = input.required(); + selectedLicense = model(null); + createLicense = output<{ id: string; licenseOptions: LicenseOptions }>(); + selectLicense = output(); + protected inputLimits = InputLimits; + + currentYear = new Date(); + licenseForm!: FormGroup; + + constructor() { + effect(() => { + const license = this.licenses().find((l) => l.id === this.selectedLicenseId()); + this.selectedLicense.set(license || null); + }); + + effect(() => { + const options = this.selectedLicenseOptions(); + if (this.selectedLicenseOptions()) { + this.licenseForm.patchValue({ + year: options!.year, + copyrightHolder: options!.copyrightHolder, + }); + } + }); + + this.initForm(); + } + + onSelectLicense(license: License): void { + if (license.requiredFields.length) { + return; + } + + this.selectLicense.emit(license); + } + + saveLicense() { + if (this.licenseForm.invalid) { + return; + } + const selectedLicenseId = this.selectedLicense()!.id; + + const model = this.licenseForm.value as LicenseOptions; + this.createLicense.emit({ + id: selectedLicenseId, + licenseOptions: model, + }); + } + + cancel() { + this.selectedLicense.set(null); + this.licenseForm.reset({ + year: this.currentYear.getFullYear().toString(), + copyrightHolder: '', + }); + } + + private initForm() { + this.licenseForm = new FormGroup({ + year: new FormControl(this.currentYear.getFullYear().toString(), { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed()], + }), + copyrightHolder: new FormControl('', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed()], + }), + }); + } +} diff --git a/src/app/shared/components/license/models/index.ts b/src/app/shared/components/license/models/index.ts new file mode 100644 index 000000000..10c39d296 --- /dev/null +++ b/src/app/shared/components/license/models/index.ts @@ -0,0 +1 @@ +export * from './license-form.models'; diff --git a/src/app/shared/components/license/models/license-form.models.ts b/src/app/shared/components/license/models/license-form.models.ts new file mode 100644 index 000000000..476c4fb46 --- /dev/null +++ b/src/app/shared/components/license/models/license-form.models.ts @@ -0,0 +1,6 @@ +import { FormControl } from '@angular/forms'; + +export interface LicenseForm { + copyrightHolder: FormControl; + year: FormControl; +} diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 8c81d4fe5..5c5cd00dd 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -1,5 +1,6 @@ export * from './addon.mapper'; export * from './filters'; export * from './institutions'; +export * from './licenses.mapper'; export * from './resource-card'; export * from './subjects'; diff --git a/src/app/shared/mappers/licenses.mapper.ts b/src/app/shared/mappers/licenses.mapper.ts new file mode 100644 index 000000000..1e6d83036 --- /dev/null +++ b/src/app/shared/mappers/licenses.mapper.ts @@ -0,0 +1,14 @@ +import { License } from '@shared/models/license.model'; +import { LicensesResponseJsonApi } from '@shared/models/licenses-json-api.model'; + +export class LicensesMapper { + static fromLicensesResponse(response: LicensesResponseJsonApi): License[] { + return response.data.map((item) => ({ + id: item.id, + name: item.attributes.name, + requiredFields: item.attributes.required_fields, + url: item.attributes.url, + text: item.attributes.text, + })); + } +} diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index bebcc60f3..a41243b27 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -13,6 +13,8 @@ export * from './id-name.model'; export * from './institutions'; export * from './language-code.model'; export * from './license.model'; +export * from './license.model'; +export * from './licenses-json-api.model'; export * from './metadata-field.model'; export * from './nav-item.model'; export * from './node-response.model'; diff --git a/src/app/shared/models/license.model.ts b/src/app/shared/models/license.model.ts index f10f5f15f..5d78d5935 100644 --- a/src/app/shared/models/license.model.ts +++ b/src/app/shared/models/license.model.ts @@ -1,34 +1,12 @@ -export interface LicenseAttributes { - name: string; - text: string; - url: string; - required_fields: string[]; -} - -export interface LicenseLinks { - self: string; -} - export interface License { id: string; - type: string; - attributes: LicenseAttributes; - links: LicenseLinks; + name: string; + requiredFields: string[]; + url: string; + text: string; } -export interface LicensesResponseJsonApi { - data: License[]; - links: { - first: string | null; - last: string | null; - prev: string | null; - next: string | null; - meta: { - total: number; - per_page: number; - }; - }; - meta: { - version: string; - }; +export interface LicenseOptions { + copyrightHolder: string; + year: string; } diff --git a/src/app/shared/models/licenses-json-api.model.ts b/src/app/shared/models/licenses-json-api.model.ts new file mode 100644 index 000000000..356f06d0f --- /dev/null +++ b/src/app/shared/models/licenses-json-api.model.ts @@ -0,0 +1,21 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi } from '@core/models'; + +export interface LicensesResponseJsonApi { + data: LicenseDataJsonApi[]; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type LicenseDataJsonApi = ApiData; + +export interface LicenseAttributesJsonApi { + name: string; + required_fields: string[]; + url: string; + text: string; +} + +export interface LicenseRecordJsonApi { + copyright_holders: string[]; + year: string; +} diff --git a/src/app/shared/pipes/index.ts b/src/app/shared/pipes/index.ts index 05dc24e77..6e3ada82f 100644 --- a/src/app/shared/pipes/index.ts +++ b/src/app/shared/pipes/index.ts @@ -1,4 +1,5 @@ export { DecodeHtmlPipe } from './decode-html.pipe'; export { FileSizePipe } from './file-size.pipe'; +export { InterpolatePipe } from './interpolate.pipe'; export { MonthYearPipe } from './month-year.pipe'; export { WrapFnPipe } from './wrap-fn.pipe'; diff --git a/src/app/shared/pipes/interpolate.pipe.ts b/src/app/shared/pipes/interpolate.pipe.ts new file mode 100644 index 000000000..9a42f81ec --- /dev/null +++ b/src/app/shared/pipes/interpolate.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'interpolate', +}) +export class InterpolatePipe implements PipeTransform { + transform(template: string, variables: Record): string { + return template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => (variables[key] != null ? variables[key] : '')); + } +} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index ea718bdae..0bb43c091 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -3,6 +3,7 @@ export { CustomConfirmationService } from './custom-confirmation.service'; export { FilesService } from './files.service'; export { FiltersOptionsService } from './filters-options.service'; export { InstitutionsService } from './institutions.service'; +export { LicensesService } from './licenses.service'; export { LoaderService } from './loader.service'; export { ResourceCardService } from './resource-card.service'; export { SearchService } from './search.service'; diff --git a/src/app/shared/services/licenses.service.ts b/src/app/shared/services/licenses.service.ts new file mode 100644 index 000000000..0b5bd0a01 --- /dev/null +++ b/src/app/shared/services/licenses.service.ts @@ -0,0 +1,23 @@ +import { map, Observable } from 'rxjs'; + +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; + +import { LicensesMapper } from '@shared/mappers'; +import { License, LicensesResponseJsonApi } from '@shared/models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class LicensesService { + private readonly http = inject(HttpClient); + private readonly baseUrl = environment.apiUrl; + + getAllLicenses(): Observable { + return this.http + .get(`${this.baseUrl}/licenses/?page[size]=20`) + .pipe(map((licenses) => LicensesMapper.fromLicensesResponse(licenses))); + } +} diff --git a/src/app/shared/stores/licenses/licenses.state.ts b/src/app/shared/stores/licenses/licenses.state.ts index 6c4e4461a..4cbd08514 100644 --- a/src/app/shared/stores/licenses/licenses.state.ts +++ b/src/app/shared/stores/licenses/licenses.state.ts @@ -5,7 +5,7 @@ import { catchError, tap } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; -import { LicensesService } from '@core/services/licenses.service'; +import { LicensesService } from '@shared/services'; import { LoadAllLicenses } from './licenses.actions'; import { LicensesStateModel } from './licenses.model'; @@ -37,10 +37,10 @@ export class LicensesState { }); return this.licensesService.getAllLicenses().pipe( - tap((response) => { + tap((data) => { ctx.patchState({ licenses: { - data: response.data, + data: data, isLoading: false, error: null, }, diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2ab43f0cd..e337bf599 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -38,12 +38,17 @@ } }, "labels": { - "downloads": "Downloads" + "downloads": "Downloads", + "year": "Year" }, "deleteConfirmation": { "header": "Delete", "message": "Are you sure you want to proceed?" }, + "links": { + "clickHere": "Click here", + "helpGuide": "Help Guide" + }, "placeholder": { "addTag": "Add tags" } @@ -1566,6 +1571,13 @@ "materials": "Materials", "papers": "Papers", "supplements": "Supplements" + }, + "license": { + "title": "License", + "selectLicense": "Select license", + "description": "A license tells others how they can use your work in the future and only applies to the information and files submitted with the registration.", + "helpText": "For more information, see this ", + "copyrightHolders": "Copyright Holders" } }, "pageNotFound": { From b75101f66036c45baa95ce05a33635ae938234e4 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 1 Jul 2025 13:50:55 +0300 Subject: [PATCH 32/35] feat(licenses): Updated save button label, disabling button after save --- src/app/core/services/index.ts | 1 - .../preprints/mappers/preprints.mapper.ts | 2 +- .../preprints/services/licenses.service.ts | 2 +- .../components/license/license.component.html | 6 +- .../components/license/license.component.ts | 58 ++++++++++++------- .../license/models/license-form.models.ts | 2 +- src/app/shared/models/license.model.ts | 2 +- 7 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/app/core/services/index.ts b/src/app/core/services/index.ts index 55d64238d..9d432f280 100644 --- a/src/app/core/services/index.ts +++ b/src/app/core/services/index.ts @@ -1,4 +1,3 @@ export { JsonApiService } from './json-api.service'; export { RequestAccessService } from './request-access.service'; export { UserService } from './user.service'; -export { LicensesService } from '@shared/services/licenses.service'; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index e16bbfa22..25e3a5cd1 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -98,7 +98,7 @@ export class PreprintsMapper { licenseOptions: response.attributes.license_record ? { year: response.attributes.license_record.year, - copyrightHolder: response.attributes.license_record.copyright_holders[0], + copyrightHolders: response.attributes.license_record.copyright_holders.join(','), } : null, }; diff --git a/src/app/features/preprints/services/licenses.service.ts b/src/app/features/preprints/services/licenses.service.ts index 5238a07ff..06b62098c 100644 --- a/src/app/features/preprints/services/licenses.service.ts +++ b/src/app/features/preprints/services/licenses.service.ts @@ -47,7 +47,7 @@ export class LicensesService { attributes: { ...(licenseOptions && { license_record: { - copyright_holders: [licenseOptions.copyrightHolder], + copyright_holders: [licenseOptions.copyrightHolders], year: licenseOptions.year, }, }), diff --git a/src/app/shared/components/license/license.component.html b/src/app/shared/components/license/license.component.html index 8d03eca44..da48a4ce0 100644 --- a/src/app/shared/components/license/license.component.html +++ b/src/app/shared/components/license/license.component.html @@ -35,7 +35,7 @@

{{ 'shared.license.title' | translate }}

@@ -57,8 +57,8 @@

{{ 'shared.license.title' | translate }}

diff --git a/src/app/shared/components/license/license.component.ts b/src/app/shared/components/license/license.component.ts index cee3225c9..3f6dba7eb 100644 --- a/src/app/shared/components/license/license.component.ts +++ b/src/app/shared/components/license/license.component.ts @@ -6,17 +6,20 @@ import { DatePicker } from 'primeng/datepicker'; import { Divider } from 'primeng/divider'; import { Select } from 'primeng/select'; -import { ChangeDetectionStrategy, Component, effect, input, model, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, input, model, output, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { StringOrNullOrUndefined } from '@core/helpers'; -import { TextInputComponent, TruncatedTextComponent } from '@shared/components'; -import { LicenseForm } from '@shared/components/license/models'; import { InputLimits } from '@shared/constants'; import { License, LicenseOptions } from '@shared/models'; import { InterpolatePipe } from '@shared/pipes'; import { CustomValidators } from '@shared/utils'; +import { LicenseForm } from '../license/models'; +import { TextInputComponent } from '../text-input/text-input.component'; +import { TruncatedTextComponent } from '../truncated-text/truncated-text.component'; + @Component({ selector: 'osf-license', imports: [ @@ -25,11 +28,11 @@ import { CustomValidators } from '@shared/utils'; Select, FormsModule, Divider, - TruncatedTextComponent, DatePicker, TextInputComponent, ReactiveFormsModule, Button, + TruncatedTextComponent, InterpolatePipe, ], templateUrl: './license.component.html', @@ -44,9 +47,21 @@ export class LicenseComponent { createLicense = output<{ id: string; licenseOptions: LicenseOptions }>(); selectLicense = output(); protected inputLimits = InputLimits; + saveButtonDisabled = signal(false); + //saveButtonCLickedAtLeastOnce = signal(false); currentYear = new Date(); - licenseForm!: FormGroup; + licenseForm = new FormGroup({ + year: new FormControl(this.currentYear.getFullYear().toString(), { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed()], + }), + copyrightHolders: new FormControl('', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed()], + }), + }); + licenseFormValue = toSignal(this.licenseForm.valueChanges); constructor() { effect(() => { @@ -59,12 +74,25 @@ export class LicenseComponent { if (this.selectedLicenseOptions()) { this.licenseForm.patchValue({ year: options!.year, - copyrightHolder: options!.copyrightHolder, + copyrightHolders: options!.copyrightHolders, }); } }); - this.initForm(); + effect(() => { + const licenseOptionsInput = this.selectedLicenseOptions(); + const licenseOptionsFormValue = this.licenseFormValue(); + + if (!this.selectedLicense() || !this.selectedLicense()?.requiredFields.length) { + return; + } + + if (JSON.stringify(licenseOptionsInput) === JSON.stringify(licenseOptionsFormValue)) { + this.saveButtonDisabled.set(true); + } else { + this.saveButtonDisabled.set(false); + } + }); } onSelectLicense(license: License): void { @@ -89,23 +117,9 @@ export class LicenseComponent { } cancel() { - this.selectedLicense.set(null); this.licenseForm.reset({ year: this.currentYear.getFullYear().toString(), - copyrightHolder: '', - }); - } - - private initForm() { - this.licenseForm = new FormGroup({ - year: new FormControl(this.currentYear.getFullYear().toString(), { - nonNullable: true, - validators: [CustomValidators.requiredTrimmed()], - }), - copyrightHolder: new FormControl('', { - nonNullable: true, - validators: [CustomValidators.requiredTrimmed()], - }), + copyrightHolders: '', }); } } diff --git a/src/app/shared/components/license/models/license-form.models.ts b/src/app/shared/components/license/models/license-form.models.ts index 476c4fb46..8757d305f 100644 --- a/src/app/shared/components/license/models/license-form.models.ts +++ b/src/app/shared/components/license/models/license-form.models.ts @@ -1,6 +1,6 @@ import { FormControl } from '@angular/forms'; export interface LicenseForm { - copyrightHolder: FormControl; + copyrightHolders: FormControl; year: FormControl; } diff --git a/src/app/shared/models/license.model.ts b/src/app/shared/models/license.model.ts index 5d78d5935..8206e7fb8 100644 --- a/src/app/shared/models/license.model.ts +++ b/src/app/shared/models/license.model.ts @@ -7,6 +7,6 @@ export interface License { } export interface LicenseOptions { - copyrightHolder: string; + copyrightHolders: string; year: string; } From 29c06bb743874278d598899a4a0b3bd714fb92a0 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 1 Jul 2025 14:57:29 +0300 Subject: [PATCH 33/35] fix(comments): Fixed comments --- .../file-step/file-step.component.ts | 19 ++++++-------- .../metadata/metadata.component.ts | 3 ++- .../submit-preprint-stepper.component.html | 14 +++++------ .../submit-preprint-stepper.component.ts | 4 ++- .../components/license/license.component.ts | 1 - .../models/confirmation-options.model.ts | 12 +++++++++ .../services/custom-confirmation.service.ts | 25 ++++++++++++++++++- src/assets/i18n/en.json | 1 + 8 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts index 35d45c2e6..315e832d2 100644 --- a/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/submit-steps/file-step/file-step.component.ts @@ -1,6 +1,5 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { ConfirmationService } from 'primeng/api'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { DialogService } from 'primeng/dynamicdialog'; @@ -41,7 +40,7 @@ import { import { FilesTreeActions } from '@osf/features/project/files/models'; import { FilesTreeComponent, IconComponent } from '@shared/components'; import { OsfFile } from '@shared/models'; -import { defaultConfirmationConfig } from '@shared/utils'; +import { CustomConfirmationService } from '@shared/services'; @Component({ selector: 'osf-file-step', @@ -63,7 +62,7 @@ import { defaultConfirmationConfig } from '@shared/utils'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileStepComponent implements OnInit { - private confirmationService = inject(ConfirmationService); + private customConfirmationService = inject(CustomConfirmationService); private actions = createDispatchMap({ setSelectedFileSource: SetSelectedPreprintFileSource, getPreprintFilesLinks: GetPreprintFilesLinks, @@ -182,19 +181,15 @@ export class FileStepComponent implements OnInit { } versionFile() { - this.confirmationService.confirm({ - ...defaultConfirmationConfig, - header: 'Add a new preprint file', - message: + this.customConfirmationService.confirmContinue({ + headerKey: 'Add a new preprint file', + messageKey: 'This will allow a new version of the preprint file to be uploaded to the preprint. The existing file will be retained as a version of the preprint.', - acceptButtonProps: { - label: 'Continue', - severity: 'danger', - }, - accept: () => { + onConfirm: () => { this.versionFileMode.set(true); this.actions.setSelectedFileSource(PreprintFileSource.None); }, + onReject: () => null, }); } } diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts index 33da63197..53f574891 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts @@ -12,7 +12,6 @@ import { Tooltip } from 'primeng/tooltip'; import { ChangeDetectionStrategy, Component, HostListener, OnInit, output } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { ContributorsComponent } from '@osf/features/preprints/components/submit-steps/metadata/contributors/contributors.component'; import { formInputLimits } from '@osf/features/preprints/constants'; import { MetadataForm, Preprint } from '@osf/features/preprints/models'; import { @@ -27,6 +26,8 @@ import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; import { License, LicenseOptions } from '@shared/models'; import { CustomValidators, findChangedFields } from '@shared/utils'; +import { ContributorsComponent } from './contributors/contributors.component'; + @Component({ selector: 'osf-preprint-metadata', imports: [ 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 93c84fd36..da30ac767 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 @@ -30,17 +30,17 @@

{{ 'Add a ' + preprintProvider()!.preprintWor
@switch (currentStep()) { - @case (0) { - + @case (SubmitStepsEnum.TitleAndAbstract) { + } - @case (1) { + @case (SubmitStepsEnum.File) { } - @case (2) { - + @case (SubmitStepsEnum.Metadata) { + } @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 31521338e..1b62fbbcc 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 @@ -23,6 +23,7 @@ import { TitleAndAbstractStepComponent, } from '@osf/features/preprints/components'; import { submitPreprintSteps } from '@osf/features/preprints/constants'; +import { SubmitSteps } from '@osf/features/preprints/enums'; import { BrandService } from '@osf/features/preprints/services'; import { GetPreprintProviderById, PreprintsSelectors } from '@osf/features/preprints/store/preprints'; import { @@ -52,11 +53,12 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy { resetStateAndDeletePreprint: ResetStateAndDeletePreprint, }); + readonly SubmitStepsEnum = SubmitSteps; readonly submitPreprintSteps = submitPreprintSteps; preprintProvider = select(PreprintsSelectors.getPreprintProviderDetails(this.providerId())); isPreprintProviderLoading = select(PreprintsSelectors.isPreprintProviderDetailsLoading); - currentStep = signal(2); + currentStep = signal(0); isWeb = toSignal(inject(IS_WEB)); constructor() { diff --git a/src/app/shared/components/license/license.component.ts b/src/app/shared/components/license/license.component.ts index 3f6dba7eb..27eb3f58f 100644 --- a/src/app/shared/components/license/license.component.ts +++ b/src/app/shared/components/license/license.component.ts @@ -48,7 +48,6 @@ export class LicenseComponent { selectLicense = output(); protected inputLimits = InputLimits; saveButtonDisabled = signal(false); - //saveButtonCLickedAtLeastOnce = signal(false); currentYear = new Date(); licenseForm = new FormGroup({ diff --git a/src/app/shared/models/confirmation-options.model.ts b/src/app/shared/models/confirmation-options.model.ts index e051a3d16..01b0d28e8 100644 --- a/src/app/shared/models/confirmation-options.model.ts +++ b/src/app/shared/models/confirmation-options.model.ts @@ -21,3 +21,15 @@ export interface AcceptConfirmationOptions { onConfirm: () => void; onReject: () => void; } + +export interface ContinueConfirmationOptions { + headerKey: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + headerParams?: any; + messageKey: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + messageParams?: any; + acceptLabelKey?: string; + onConfirm: () => void; + onReject: () => void; +} diff --git a/src/app/shared/services/custom-confirmation.service.ts b/src/app/shared/services/custom-confirmation.service.ts index d828a42ff..49028e7d5 100644 --- a/src/app/shared/services/custom-confirmation.service.ts +++ b/src/app/shared/services/custom-confirmation.service.ts @@ -4,7 +4,7 @@ import { ConfirmationService } from 'primeng/api'; import { inject, Injectable } from '@angular/core'; -import { AcceptConfirmationOptions, DeleteConfirmationOptions } from '../models'; +import { AcceptConfirmationOptions, ContinueConfirmationOptions, DeleteConfirmationOptions } from '../models'; @Injectable({ providedIn: 'root', @@ -54,4 +54,27 @@ export class CustomConfirmationService { }, }); } + + confirmContinue(options: ContinueConfirmationOptions): void { + this.confirmationService.confirm({ + header: this.translateService.instant(options.headerKey, options.headerParams), + message: this.translateService.instant(options.messageKey, options.messageParams), + closable: true, + closeOnEscape: false, + acceptButtonProps: { + label: this.translateService.instant(options.acceptLabelKey || 'common.buttons.continue'), + severity: 'danger', + }, + rejectButtonProps: { + label: this.translateService.instant('common.buttons.cancel'), + severity: 'info', + }, + accept: () => { + options.onConfirm(); + }, + reject: () => { + options.onReject(); + }, + }); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 12f4cbee1..2e16b30cd 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -20,6 +20,7 @@ "disconnect": "Disconnect", "revert": "Revert", "next": "Next", + "continue": "Continue", "skip": "Skip", "done": "Done", "select": "Select", From 6ff200bf059c67a11b7fe6c4c947534a4fa25959 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 1 Jul 2025 15:42:37 +0300 Subject: [PATCH 34/35] feat(tags): Added tags to metadata step --- .../submit-steps/metadata/metadata.component.html | 13 ++++++++++++- .../submit-steps/metadata/metadata.component.ts | 15 ++++++++++++++- .../models/submit-preprint-form.models.ts | 1 + .../preprints/services/preprints.service.ts | 1 + .../submit-preprint/submit-preprint.state.ts | 4 ++-- 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html index 9c8bc9aac..682a46bb2 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.html @@ -33,6 +33,16 @@

Publication DOI

+ +
+

Tags (optional)

+ +
+ +
+
+
+

Publication Date (optional)

@@ -69,7 +79,8 @@

Publication Citation (optional)

label="Next" [pTooltip]="metadataForm.invalid ? 'Fill in \'Required\' fields to continue' : ''" tooltipPosition="top" - [disabled]="metadataForm.invalid" + [disabled]="metadataForm.invalid || !createdPreprint()?.licenseId" + [loading]="isUpdatingPreprint()" (click)="nextButtonClicked()" />
diff --git a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts index 53f574891..c82bb1e9a 100644 --- a/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts +++ b/src/app/features/preprints/components/submit-steps/metadata/metadata.component.ts @@ -21,7 +21,7 @@ import { SubmitPreprintSelectors, UpdatePreprint, } from '@osf/features/preprints/store/submit-preprint'; -import { IconComponent, LicenseComponent, TextInputComponent } from '@shared/components'; +import { IconComponent, LicenseComponent, TagsInputComponent, TextInputComponent } from '@shared/components'; import { INPUT_VALIDATION_MESSAGES } from '@shared/constants'; import { License, LicenseOptions } from '@shared/models'; import { CustomValidators, findChangedFields } from '@shared/utils'; @@ -43,6 +43,7 @@ import { ContributorsComponent } from './contributors/contributors.component'; TextInputComponent, Tooltip, LicenseComponent, + TagsInputComponent, ], templateUrl: './metadata.component.html', styleUrl: './metadata.component.scss', @@ -62,6 +63,8 @@ export class MetadataComponent implements OnInit { licences = select(SubmitPreprintSelectors.getLicenses); createdPreprint = select(SubmitPreprintSelectors.getCreatedPreprint); + isUpdatingPreprint = select(SubmitPreprintSelectors.isPreprintSubmitting); + nextClicked = output(); ngOnInit() { @@ -84,6 +87,10 @@ export class MetadataComponent implements OnInit { nonNullable: false, validators: [Validators.maxLength(this.inputLimits.citation.maxLength)], }), + tags: new FormControl(this.createdPreprint()?.tags || [], { + nonNullable: true, + validators: [], + }), }); } @@ -116,4 +123,10 @@ export class MetadataComponent implements OnInit { selectLicense(license: License) { this.actions.saveLicense(license.id); } + + updateTags(updatedTags: string[]) { + this.metadataForm.patchValue({ + tags: updatedTags, + }); + } } diff --git a/src/app/features/preprints/models/submit-preprint-form.models.ts b/src/app/features/preprints/models/submit-preprint-form.models.ts index 4c2a128db..d85d06705 100644 --- a/src/app/features/preprints/models/submit-preprint-form.models.ts +++ b/src/app/features/preprints/models/submit-preprint-form.models.ts @@ -11,4 +11,5 @@ export interface MetadataForm { doi: FormControl; originalPublicationDate: FormControl; customPublicationCitation: FormControl; + tags: FormControl; } diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index a5e146bd0..adc0e74b4 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -36,6 +36,7 @@ export class PreprintsService { originalPublicationDate: 'original_publication_date', doi: 'doi', customPublicationCitation: 'custom_publication_citation', + tags: 'tags', }; getPreprintProviderById(id: string): Observable { diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index 7addc2a61..ae64ca981 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -42,7 +42,7 @@ import { defaults: { selectedProviderId: null, createdPreprint: { - data: { id: '6s4jg_v1' } as Preprint, // Temporary default value for testing + data: null, isLoading: false, error: null, isSubmitting: false, @@ -317,7 +317,7 @@ export class SubmitPreprintState { }, }); if (createdPreprintId) { - return this.preprintsService.deletePreprint(createdPreprintId); + //return this.preprintsService.deletePreprint(createdPreprintId); } return EMPTY; From de923639c5a53da3e1c26bcdb5b1ad2ee9d44634 Mon Sep 17 00:00:00 2001 From: Roma Date: Tue, 1 Jul 2025 15:46:21 +0300 Subject: [PATCH 35/35] fix(state): Uncommented code --- .../preprints/store/submit-preprint/submit-preprint.state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts index ae64ca981..10f98cf1c 100644 --- a/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts +++ b/src/app/features/preprints/store/submit-preprint/submit-preprint.state.ts @@ -317,7 +317,7 @@ export class SubmitPreprintState { }, }); if (createdPreprintId) { - //return this.preprintsService.deletePreprint(createdPreprintId); + return this.preprintsService.deletePreprint(createdPreprintId); } return EMPTY;