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",