diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index d27777895..be7778702 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -56,7 +56,7 @@ export const MENU_ITEMS: MenuItem[] = [ routerLinkActiveOptions: { exact: false }, }, { - routerLink: '/my-preprints', + routerLink: 'preprints/my-preprints', label: 'navigation.preprintsSubRoutes.myPreprints', routerLinkActiveOptions: { exact: false }, }, diff --git a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html b/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html index ab3719ec2..a7538e5eb 100644 --- a/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html +++ b/src/app/features/preprints/components/stepper/metadata-step/metadata-step.component.html @@ -96,7 +96,9 @@

{{ 'preprints.preprintStepper.metadata.publicationCitationTit class="w-6 md:w-9rem" styleClass="w-full" [label]="'common.buttons.next' | translate" - [pTooltip]="metadataForm.invalid ? ('preprintStepper.common.validation.fillRequiredFields' | translate) : ''" + [pTooltip]=" + metadataForm.invalid ? ('preprints.preprintStepper.common.validation.fillRequiredFields' | translate) : '' + " tooltipPosition="top" [disabled]="metadataForm.invalid || !createdPreprint()?.licenseId" [loading]="isUpdatingPreprint()" diff --git a/src/app/features/preprints/constants/index.ts b/src/app/features/preprints/constants/index.ts index 5eb8b968b..45057e6ea 100644 --- a/src/app/features/preprints/constants/index.ts +++ b/src/app/features/preprints/constants/index.ts @@ -1,5 +1,6 @@ export * from './create-new-version-steps.const'; export * from './form-input-limits.const'; +export * from './preprints-fields.const'; export * from './prereg-link-options.const'; export * from './submit-preprint-steps.const'; export * from './update-preprint-steps.const'; diff --git a/src/app/features/preprints/constants/preprints-fields.const.ts b/src/app/features/preprints/constants/preprints-fields.const.ts new file mode 100644 index 000000000..a9f258b40 --- /dev/null +++ b/src/app/features/preprints/constants/preprints-fields.const.ts @@ -0,0 +1,4 @@ +export const preprintSortFieldMap: Record = { + title: 'title', + dateModified: 'date_modified', +}; diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts index f747eaa8d..34c7f4951 100644 --- a/src/app/features/preprints/mappers/preprints.mapper.ts +++ b/src/app/features/preprints/mappers/preprints.mapper.ts @@ -1,5 +1,11 @@ -import { ApiData } from '@core/models'; -import { Preprint, PreprintJsonApi, PreprintsRelationshipsJsonApi } from '@osf/features/preprints/models'; +import { ApiData, JsonApiResponseWithPaging } from '@core/models'; +import { + Preprint, + PreprintAttributesJsonApi, + PreprintEmbedsJsonApi, + PreprintRelationshipsJsonApi, + PreprintShortInfoWithTotalCount, +} from '@osf/features/preprints/models'; export class PreprintsMapper { static toCreatePayload(title: string, abstract: string, providerId: string) { @@ -22,7 +28,9 @@ export class PreprintsMapper { }; } - static fromPreprintJsonApi(response: ApiData): Preprint { + static fromPreprintJsonApi( + response: ApiData + ): Preprint { return { id: response.id, dateCreated: response.attributes.date_created, @@ -76,4 +84,25 @@ export class PreprintsMapper { }, }; } + + static fromMyPreprintJsonApi( + response: JsonApiResponseWithPaging[], null> + ): PreprintShortInfoWithTotalCount { + return { + data: response.data.map((preprintData) => { + return { + id: preprintData.id, + title: preprintData.attributes.title, + dateModified: preprintData.attributes.date_modified, + contributors: preprintData.embeds.bibliographic_contributors.data.map((contrData) => { + return { + id: contrData.id, + name: contrData.embeds.users.data.attributes.full_name, + }; + }), + }; + }), + totalCount: response.links.meta.total, + }; + } } diff --git a/src/app/features/preprints/models/preprint-json-api.models.ts b/src/app/features/preprints/models/preprint-json-api.models.ts index f09523304..36fa5ab84 100644 --- a/src/app/features/preprints/models/preprint-json-api.models.ts +++ b/src/app/features/preprints/models/preprint-json-api.models.ts @@ -1,8 +1,8 @@ import { BooleanOrNull, StringOrNull } from '@core/helpers'; import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums'; -import { LicenseRecordJsonApi } from '@shared/models'; +import { ContributorResponse, LicenseRecordJsonApi } from '@shared/models'; -export interface PreprintJsonApi { +export interface PreprintAttributesJsonApi { date_created: string; date_modified: string; date_published: Date | null; @@ -34,7 +34,7 @@ export interface PreprintJsonApi { prereg_link_info: PreregLinkInfo | null; } -export interface PreprintsRelationshipsJsonApi { +export interface PreprintRelationshipsJsonApi { primary_file: { data: { id: string; @@ -54,3 +54,9 @@ export interface PreprintsRelationshipsJsonApi { }; }; } + +export interface PreprintEmbedsJsonApi { + bibliographic_contributors: { + data: ContributorResponse[]; + }; +} diff --git a/src/app/features/preprints/models/preprint.models.ts b/src/app/features/preprints/models/preprint.models.ts index d791c2d23..ee93fc534 100644 --- a/src/app/features/preprints/models/preprint.models.ts +++ b/src/app/features/preprints/models/preprint.models.ts @@ -1,6 +1,6 @@ import { BooleanOrNull, StringOrNull } from '@core/helpers'; import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums'; -import { LicenseOptions } from '@shared/models'; +import { IdName, LicenseOptions } from '@shared/models'; export interface Preprint { id: string; @@ -35,3 +35,15 @@ export interface PreprintFilesLinks { filesLink: string; uploadFileLink: string; } + +export interface PreprintShortInfo { + id: string; + title: string; + dateModified: string; + contributors: IdName[]; +} + +export interface PreprintShortInfoWithTotalCount { + data: PreprintShortInfo[]; + totalCount: number; +} diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.html b/src/app/features/preprints/pages/my-preprints/my-preprints.component.html new file mode 100644 index 000000000..f95872d78 --- /dev/null +++ b/src/app/features/preprints/pages/my-preprints/my-preprints.component.html @@ -0,0 +1,74 @@ + + +
+ + + + + + + {{ 'preprints.myPreprints.table.titleLabel' | translate }} + + + {{ 'preprints.myPreprints.table.contributorsLabel' | translate }} + + {{ 'preprints.myPreprints.table.modifiedLabel' | translate }} + + + + + + @if (item?.id) { + + {{ item.title }} + + + + {{ item.dateModified | date: 'MMM d, y, h:mm a' }} + + } @else { + + + + + + } + + + + {{ 'common.search.noResultsFound' | translate }} + + + +
diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.scss b/src/app/features/preprints/pages/my-preprints/my-preprints.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts b/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts new file mode 100644 index 000000000..1c2ce025c --- /dev/null +++ b/src/app/features/preprints/pages/my-preprints/my-preprints.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MyPreprintsComponent } from './my-preprints.component'; + +describe('MyPreprintsComponent', () => { + let component: MyPreprintsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MyPreprintsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MyPreprintsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts b/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts new file mode 100644 index 000000000..ab0c5fa07 --- /dev/null +++ b/src/app/features/preprints/pages/my-preprints/my-preprints.component.ts @@ -0,0 +1,194 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { SortEvent } from 'primeng/api'; +import { Skeleton } from 'primeng/skeleton'; +import { TableModule, TablePageEvent } from 'primeng/table'; + +import { debounceTime, distinctUntilChanged, skip } from 'rxjs'; + +import { DatePipe, TitleCasePipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + effect, + HostBinding, + inject, + signal, + untracked, +} from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { parseQueryFilterParams } from '@core/helpers'; +import { Preprint, PreprintShortInfo } from '@osf/features/preprints/models'; +import { FetchMyPreprints, PreprintSelectors } from '@osf/features/preprints/store/preprint'; +import { ListInfoShortenerComponent, SearchInputComponent, SubHeaderComponent } from '@shared/components'; +import { TABLE_PARAMS } from '@shared/constants'; +import { SortOrder } from '@shared/enums'; +import { QueryParams, SearchFilters, TableParameters } from '@shared/models'; + +@Component({ + selector: 'osf-my-preprints', + imports: [ + SubHeaderComponent, + SearchInputComponent, + TranslatePipe, + TableModule, + Skeleton, + DatePipe, + ListInfoShortenerComponent, + TitleCasePipe, + ], + templateUrl: './my-preprints.component.html', + styleUrl: './my-preprints.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyPreprintsComponent { + @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + private actions = createDispatchMap({ + fetchMyPreprints: FetchMyPreprints, + }); + + searchControl = new FormControl(''); + + queryParams = toSignal(this.route.queryParams); + sortColumn = signal(''); + sortOrder = signal(SortOrder.Asc); + currentPage = signal(1); + currentPageSize = signal(TABLE_PARAMS.rows); + tableParams = signal({ + ...TABLE_PARAMS, + firstRowIndex: 0, + }); + + preprints = select(PreprintSelectors.getMyPreprints); + preprintsTotalCount = select(PreprintSelectors.getMyPreprintsTotalCount); + areMyPreprintsLoading = select(PreprintSelectors.areMyPreprintsLoading); + skeletonData: PreprintShortInfo[] = Array.from({ length: 10 }, () => ({}) as PreprintShortInfo); + + constructor() { + this.setupTotalRecordsEffect(); + this.setupSearchSubscription(); + this.setupQueryParamsEffect(); + } + + navigateToPreprintDetails(preprint: Preprint): void { + //[RNi] TODO: Implement redirect when details page is done + } + + onPageChange(event: TablePageEvent): void { + const page = Math.floor(event.first / event.rows) + 1; + + this.updateQueryParams({ + page, + size: event.rows, + }); + } + + onSort(event: SortEvent): void { + if (event.field) { + this.updateQueryParams({ + sortColumn: event.field, + sortOrder: event.order === -1 ? SortOrder.Desc : SortOrder.Asc, + }); + } + } + + setupQueryParamsEffect(): void { + effect(() => { + const rawQueryParams = this.queryParams(); + if (!rawQueryParams) return; + + const parsedQueryParams = parseQueryFilterParams(rawQueryParams); + + this.updateComponentState(parsedQueryParams); + const filters = this.createFilters(parsedQueryParams); + this.actions.fetchMyPreprints(parsedQueryParams.page, parsedQueryParams.size, filters); + }); + } + + private setupSearchSubscription(): void { + this.searchControl.valueChanges + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef), skip(1)) + .subscribe((searchControl) => { + this.updateQueryParams({ + search: searchControl ?? '', + page: 1, + }); + }); + } + + private setupTotalRecordsEffect() { + effect(() => { + const totalRecords = this.preprintsTotalCount(); + untracked(() => { + this.updateTableParams({ totalRecords }); + }); + }); + } + + private updateTableParams(updates: Partial): void { + this.tableParams.update((current) => ({ + ...current, + ...updates, + })); + } + + private updateQueryParams(updates: Partial): void { + const queryParams: Record = {}; + + if ('page' in updates) { + queryParams['page'] = updates.page!.toString(); + } + if ('size' in updates) { + queryParams['size'] = updates.size!.toString(); + } + if ('search' in updates) { + queryParams['search'] = updates.search || undefined; + } + if ('sortColumn' in updates) { + queryParams['sortColumn'] = updates.sortColumn!; + queryParams['sortOrder'] = updates.sortOrder === SortOrder.Desc ? 'desc' : 'asc'; + } + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + } + + private updateComponentState(params: QueryParams): void { + untracked(() => { + this.currentPage.set(params.page); + this.currentPageSize.set(params.size); + this.searchControl.setValue(params.search); + this.sortColumn.set(params.sortColumn); + this.sortOrder.set(params.sortOrder); + + this.updateTableParams({ + rows: params.size, + firstRowIndex: (params.page - 1) * params.size, + }); + }); + } + + private createFilters(params: QueryParams): SearchFilters { + return { + searchValue: params.search, + searchFields: ['title', 'tags', 'description'], + sortColumn: params.sortColumn, + sortOrder: params.sortOrder, + }; + } + + addPreprintBtnClicked() { + this.router.navigateByUrl('/preprints/select'); + } +} diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 81a7df27f..1dde64cd4 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -3,6 +3,7 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; import { PreprintsComponent } from '@osf/features/preprints/preprints.component'; +import { PreprintState } from '@osf/features/preprints/store/preprint'; import { PreprintProvidersState } from '@osf/features/preprints/store/preprint-providers'; import { PreprintStepperState } from '@osf/features/preprints/store/preprint-stepper'; import { PreprintsDiscoverState } from '@osf/features/preprints/store/preprints-discover'; @@ -26,6 +27,7 @@ export const preprintsRoutes: Routes = [ PreprintStepperState, ContributorsState, SubjectsState, + PreprintState, ]), ], children: [ @@ -78,6 +80,13 @@ export const preprintsRoutes: Routes = [ ), canDeactivate: [ConfirmLeavingGuard], }, + { + path: 'my-preprints', + loadComponent: () => + import('@osf/features/preprints/pages/my-preprints/my-preprints.component').then( + (m) => m.MyPreprintsComponent + ), + }, { path: ':id/moderation', loadChildren: () => diff --git a/src/app/features/preprints/services/preprint-files.service.ts b/src/app/features/preprints/services/preprint-files.service.ts index 5a8effc95..a019b2756 100644 --- a/src/app/features/preprints/services/preprint-files.service.ts +++ b/src/app/features/preprints/services/preprint-files.service.ts @@ -7,9 +7,9 @@ import { ApiData, JsonApiResponse } from '@osf/core/models'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; import { Preprint, + PreprintAttributesJsonApi, PreprintFilesLinks, - PreprintJsonApi, - PreprintsRelationshipsJsonApi, + PreprintRelationshipsJsonApi, } from '@osf/features/preprints/models'; import { GetFileResponse, GetFilesResponse, OsfFile } from '@osf/shared/models'; import { FilesService } from '@shared/services'; @@ -25,7 +25,7 @@ export class PreprintFilesService { updateFileRelationship(preprintId: string, fileId: string): Observable { return this.jsonApiService - .patch>( + .patch>( `${environment.apiUrl}/preprints/${preprintId}/`, { data: { diff --git a/src/app/features/preprints/services/preprint-licenses.service.ts b/src/app/features/preprints/services/preprint-licenses.service.ts index b0438b7f5..e3a8e6b7b 100644 --- a/src/app/features/preprints/services/preprint-licenses.service.ts +++ b/src/app/features/preprints/services/preprint-licenses.service.ts @@ -6,9 +6,9 @@ import { ApiData } from '@core/models'; import { JsonApiService } from '@core/services'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; import { - PreprintJsonApi, + PreprintAttributesJsonApi, PreprintLicensePayloadJsonApi, - PreprintsRelationshipsJsonApi, + PreprintRelationshipsJsonApi, } from '@osf/features/preprints/models'; import { LicensesMapper } from '@shared/mappers'; import { License, LicenseOptions, LicensesResponseJsonApi } from '@shared/models'; @@ -57,7 +57,7 @@ export class PreprintLicensesService { return this.jsonApiService .patch< - ApiData + ApiData >(`${this.apiUrl}/preprints/${preprintId}/`, payload) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response))); } diff --git a/src/app/features/preprints/services/preprints-projects.service.ts b/src/app/features/preprints/services/preprints-projects.service.ts index 4c47f580f..072603c5a 100644 --- a/src/app/features/preprints/services/preprints-projects.service.ts +++ b/src/app/features/preprints/services/preprints-projects.service.ts @@ -6,7 +6,7 @@ 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'; -import { Preprint, PreprintJsonApi, PreprintsRelationshipsJsonApi } from '@osf/features/preprints/models'; +import { Preprint, PreprintAttributesJsonApi, PreprintRelationshipsJsonApi } from '@osf/features/preprints/models'; import { CreateProjectPayloadJsoApi, IdName, NodeData } from '@osf/shared/models'; import { environment } from 'src/environments/environment'; @@ -55,7 +55,7 @@ export class PreprintsProjectsService { updatePreprintProjectRelationship(preprintId: string, projectId: string): Observable { return this.jsonApiService - .patch>( + .patch>( `${environment.apiUrl}/preprints/${preprintId}/`, { data: { diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index 6d0b306ce..9d9a5ffd7 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -3,9 +3,17 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services'; -import { ApiData, JsonApiResponse } from '@osf/core/models'; +import { ApiData, JsonApiResponse, JsonApiResponseWithPaging } from '@osf/core/models'; +import { preprintSortFieldMap } from '@osf/features/preprints/constants'; import { PreprintsMapper } from '@osf/features/preprints/mappers'; -import { Preprint, PreprintJsonApi, PreprintsRelationshipsJsonApi } from '@osf/features/preprints/models'; +import { + Preprint, + PreprintAttributesJsonApi, + PreprintEmbedsJsonApi, + PreprintRelationshipsJsonApi, +} from '@osf/features/preprints/models'; +import { SearchFilters } from '@shared/models'; +import { searchPreferencesToJsonApiQueryParams } from '@shared/utils'; import { environment } from 'src/environments/environment'; @@ -37,7 +45,7 @@ export class PreprintsService { const payload = PreprintsMapper.toCreatePayload(title, abstract, providerId); return this.jsonApiService .post< - JsonApiResponse, null> + JsonApiResponse, null> >(`${environment.apiUrl}/preprints/`, payload) .pipe( map((response) => { @@ -49,7 +57,7 @@ export class PreprintsService { getById(id: string) { return this.jsonApiService .get< - JsonApiResponse, null> + JsonApiResponse, null> >(`${environment.apiUrl}/preprints/${id}/`) .pipe( map((response) => { @@ -66,7 +74,7 @@ export class PreprintsService { const apiPayload = this.mapPreprintDomainToApiPayload(payload); return this.jsonApiService - .patch>( + .patch>( `${environment.apiUrl}/preprints/${id}/`, { data: { @@ -87,12 +95,12 @@ export class PreprintsService { createNewVersion(prevVersionPreprintId: string) { return this.jsonApiService .post< - JsonApiResponse, null> + JsonApiResponse, null> >(`${environment.apiUrl}/preprints/${prevVersionPreprintId}/versions/?version=2.20`) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response.data))); } - private mapPreprintDomainToApiPayload(domainPayload: Partial): Partial { + private mapPreprintDomainToApiPayload(domainPayload: Partial): Partial { const apiPayload: Record = {}; Object.entries(domainPayload).forEach(([key, value]) => { if (value !== undefined && this.domainToApiFieldMap[key]) { @@ -101,4 +109,20 @@ export class PreprintsService { }); return apiPayload; } + + getMyPreprints(pageNumber: number, pageSize: number, filters: SearchFilters) { + const params: Record = { + 'embed[]': ['bibliographic_contributors'], + 'fields[users]': 'family_name,full_name,given_name,middle_name', + 'fields[preprints]': 'title,date_modified,public,bibliographic_contributors', + }; + + searchPreferencesToJsonApiQueryParams(params, pageNumber, pageSize, filters, preprintSortFieldMap); + + return this.jsonApiService + .get< + JsonApiResponseWithPaging[], null> + >(`${environment.apiUrl}/users/me/preprints/`, params) + .pipe(map((response) => PreprintsMapper.fromMyPreprintJsonApi(response))); + } } diff --git a/src/app/features/preprints/store/preprint/index.ts b/src/app/features/preprints/store/preprint/index.ts new file mode 100644 index 000000000..92aa04d1c --- /dev/null +++ b/src/app/features/preprints/store/preprint/index.ts @@ -0,0 +1,4 @@ +export * from './preprint.actions'; +export * from './preprint.model'; +export * from './preprint.selectors'; +export * from './preprint.state'; diff --git a/src/app/features/preprints/store/preprint/preprint.actions.ts b/src/app/features/preprints/store/preprint/preprint.actions.ts new file mode 100644 index 000000000..3fbaf5c68 --- /dev/null +++ b/src/app/features/preprints/store/preprint/preprint.actions.ts @@ -0,0 +1,17 @@ +import { SearchFilters } from '@shared/models'; + +export class FetchMyPreprints { + static readonly type = '[Preprint] Fetch My Preprints'; + + constructor( + public pageNumber: number, + public pageSize: number, + public filters: SearchFilters + ) {} +} + +export class FetchPreprintById { + static readonly type = '[Preprint] Fetch Preprint By Id'; + + constructor(public id: string) {} +} diff --git a/src/app/features/preprints/store/preprint/preprint.model.ts b/src/app/features/preprints/store/preprint/preprint.model.ts new file mode 100644 index 000000000..e9dd437eb --- /dev/null +++ b/src/app/features/preprints/store/preprint/preprint.model.ts @@ -0,0 +1,22 @@ +import { Preprint, PreprintShortInfo } from '@osf/features/preprints/models'; +import { AsyncStateModel, AsyncStateWithTotalCount } from '@shared/models'; + +export interface PreprintStateModel { + myPreprints: AsyncStateWithTotalCount; + preprint: AsyncStateModel; +} + +export const DefaultState: PreprintStateModel = { + preprint: { + data: null, + isLoading: false, + error: null, + isSubmitting: false, + }, + myPreprints: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + }, +}; diff --git a/src/app/features/preprints/store/preprint/preprint.selectors.ts b/src/app/features/preprints/store/preprint/preprint.selectors.ts new file mode 100644 index 000000000..206c8d78c --- /dev/null +++ b/src/app/features/preprints/store/preprint/preprint.selectors.ts @@ -0,0 +1,31 @@ +import { Selector } from '@ngxs/store'; + +import { PreprintStateModel } from './preprint.model'; +import { PreprintState } from './preprint.state'; + +export class PreprintSelectors { + @Selector([PreprintState]) + static getMyPreprints(state: PreprintStateModel) { + return state.myPreprints.data; + } + + @Selector([PreprintState]) + static getMyPreprintsTotalCount(state: PreprintStateModel) { + return state.myPreprints.totalCount; + } + + @Selector([PreprintState]) + static areMyPreprintsLoading(state: PreprintStateModel) { + return state.myPreprints.isLoading; + } + + @Selector([PreprintState]) + static getPreprint(state: PreprintStateModel) { + return state.preprint.data; + } + + @Selector([PreprintState]) + static isPreprintSubmitting(state: PreprintStateModel) { + return state.preprint.isSubmitting; + } +} diff --git a/src/app/features/preprints/store/preprint/preprint.state.ts b/src/app/features/preprints/store/preprint/preprint.state.ts new file mode 100644 index 000000000..9f104b5a2 --- /dev/null +++ b/src/app/features/preprints/store/preprint/preprint.state.ts @@ -0,0 +1,54 @@ +import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; + +import { tap } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@core/handlers'; +import { PreprintsService } from '@osf/features/preprints/services'; + +import { FetchMyPreprints, FetchPreprintById } from './preprint.actions'; +import { DefaultState, PreprintStateModel } from './preprint.model'; + +@State({ + name: 'preprints', + defaults: { ...DefaultState }, +}) +@Injectable() +export class PreprintState { + private preprintsService = inject(PreprintsService); + + @Action(FetchMyPreprints) + fetchMyPreprints(ctx: StateContext, action: FetchMyPreprints) { + ctx.setState(patch({ myPreprints: patch({ isLoading: true }) })); + + return this.preprintsService.getMyPreprints(action.pageNumber, action.pageSize, action.filters).pipe( + tap((preprints) => { + ctx.setState( + patch({ + myPreprints: patch({ + isLoading: false, + data: preprints.data, + totalCount: preprints.totalCount, + }), + }) + ); + }), + catchError((error) => handleSectionError(ctx, 'myPreprints', error)) + ); + } + + @Action(FetchPreprintById) + getPreprintById(ctx: StateContext, action: FetchPreprintById) { + ctx.setState(patch({ preprint: patch({ isLoading: true }) })); + + return this.preprintsService.getById(action.id).pipe( + tap((preprint) => { + ctx.setState(patch({ preprint: patch({ isLoading: false, data: preprint }) })); + }), + catchError((error) => handleSectionError(ctx, 'preprint', error)) + ); + } +} diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 95f53a51b..6bfe1951d 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -17,6 +17,7 @@ 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 { ListInfoShortenerComponent } from './list-info-shortener/list-info-shortener.component'; export { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; export { MarkdownComponent } from './markdown/markdown.component'; export { MyProjectsTableComponent } from './my-projects-table/my-projects-table.component'; diff --git a/src/app/shared/components/list-info-shortener/list-info-shortener.component.html b/src/app/shared/components/list-info-shortener/list-info-shortener.component.html new file mode 100644 index 000000000..98e915ea2 --- /dev/null +++ b/src/app/shared/components/list-info-shortener/list-info-shortener.component.html @@ -0,0 +1,23 @@ +@let dataValue = data(); +@let limitValue = limit(); + +@if (dataValue && dataValue.length > 0) { +
+ @for (item of dataValue.slice(0, limitValue); track item.id) { + {{ item.name }}{{ $last ? '' : ', ' }} + } + + @if (dataValue.length > limitValue) { +

+ {{ 'common.labels.and' | translate }} {{ dataValue.length - limitValue }} {{ 'common.labels.more' | translate }} +

+ + @for (item of dataValue.slice(limitValue); track item.id) { +
+ {{ item.name }} +
+ } +
+ } +
+} diff --git a/src/app/shared/components/list-info-shortener/list-info-shortener.component.scss b/src/app/shared/components/list-info-shortener/list-info-shortener.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/list-info-shortener/list-info-shortener.component.spec.ts b/src/app/shared/components/list-info-shortener/list-info-shortener.component.spec.ts new file mode 100644 index 000000000..1a403f4aa --- /dev/null +++ b/src/app/shared/components/list-info-shortener/list-info-shortener.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListInfoShortenerComponent } from './list-info-shortener.component'; + +describe('ListInfoShortenerComponent', () => { + let component: ListInfoShortenerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ListInfoShortenerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ListInfoShortenerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/list-info-shortener/list-info-shortener.component.ts b/src/app/shared/components/list-info-shortener/list-info-shortener.component.ts new file mode 100644 index 000000000..002b0e8a4 --- /dev/null +++ b/src/app/shared/components/list-info-shortener/list-info-shortener.component.ts @@ -0,0 +1,19 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Tooltip } from 'primeng/tooltip'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { IdName } from '@shared/models'; + +@Component({ + selector: 'osf-list-info-shortener', + imports: [Tooltip, TranslatePipe], + templateUrl: './list-info-shortener.component.html', + styleUrl: './list-info-shortener.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ListInfoShortenerComponent { + data = input([]); + limit = input(2); +} diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html index 35369890f..3b210ca68 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.html +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html @@ -42,6 +42,7 @@ (onPage)="onPageChange($event)" (onSort)="onSort($event)" [sortField]="sortColumn()" + [sortOrder]="sortOrder() === 0 ? 1 : -1" [customSort]="true" [resetPageOnSort]="false" > diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 38093816a..d601d7162 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -88,7 +88,9 @@ "available": "Available", "unavailable": "Unavailable", "notApplicable": "Not Applicable", - "none": "None" + "none": "None", + "and": "and", + "more": "more" }, "deleteConfirmation": { "header": "Delete", @@ -1847,7 +1849,16 @@ } }, "providers": "Providers", - "createNewVersionTitle": "Create New Version" + "createNewVersionTitle": "Create New Version", + "myPreprints": { + "title": "My Preprints", + "searchPlaceholder": "Filter by title, description, and tags", + "table": { + "titleLabel": "Title", + "contributorsLabel": "Contributors", + "modifiedLabel": "Modified" + } + } }, "registries": { "title": "Registries",