From ac1932dd958ec6d7a5421c50357507c07f15078d Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 25 Jul 2025 19:22:50 +0300 Subject: [PATCH 1/4] feat(submissions): added submissions to moderation --- .../features/moderation/components/index.ts | 2 + ...egistry-pending-submissions.component.html | 58 ++++++++++ ...egistry-pending-submissions.component.scss | 13 +++ ...stry-pending-submissions.component.spec.ts | 22 ++++ .../registry-pending-submissions.component.ts | 108 ++++++++++++++++++ .../registry-submission-item.component.html | 46 ++++++++ .../registry-submission-item.component.scss | 24 ++++ ...registry-submission-item.component.spec.ts | 22 ++++ .../registry-submission-item.component.ts | 33 ++++++ .../registry-submissions.component.html | 32 +++++- .../registry-submissions.component.scss | 13 ++- .../registry-submissions.component.ts | 91 ++++++++++++--- .../features/moderation/constants/index.ts | 1 + .../constants/registry-sort-options.const.ts | 22 ++++ .../moderation/constants/submission.const.ts | 42 ++++++- src/app/features/moderation/enums/index.ts | 1 + .../moderation/enums/registry-sort.enum.ts | 6 + .../enums/submission-review-status.enum.ts | 2 + src/app/features/moderation/mappers/index.ts | 1 + .../mappers/registry-moderation.mapper.ts | 44 +++++++ src/app/features/moderation/models/index.ts | 4 + .../models/registry-action-json-api.model.ts | 36 ++++++ .../models/registry-action.model.ts | 10 ++ .../models/registry-json-api.model.ts | 17 +++ .../models/registry-moderation.model.ts | 11 ++ .../moderation/registry-moderation.routes.ts | 6 +- src/app/features/moderation/services/index.ts | 1 + .../services/registry-moderation.service.ts | 45 ++++++++ .../store/registry-moderation/index.ts | 4 + .../registry-moderation.actions.ts | 14 +++ .../registry-moderation.model.ts | 16 +++ .../registry-moderation.selectors.ts | 21 ++++ .../registry-moderation.state.ts | 69 +++++++++++ .../bar-chart/bar-chart.component.html | 2 +- .../line-chart/line-chart.component.html | 2 +- .../components/select/select.component.html | 1 + .../components/select/select.component.ts | 1 + src/app/shared/pipes/date-ago.pipe.ts | 35 ++++++ src/app/shared/pipes/index.ts | 1 + src/app/shared/pipes/month-year.pipe.spec.ts | 8 -- src/app/shared/pipes/wrap-fn.pipe.spec.ts | 8 -- src/assets/i18n/en.json | 21 +++- 42 files changed, 868 insertions(+), 48 deletions(-) create mode 100644 src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html create mode 100644 src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.scss create mode 100644 src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts create mode 100644 src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts create mode 100644 src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html create mode 100644 src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss create mode 100644 src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts create mode 100644 src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts create mode 100644 src/app/features/moderation/constants/registry-sort-options.const.ts create mode 100644 src/app/features/moderation/enums/registry-sort.enum.ts create mode 100644 src/app/features/moderation/mappers/registry-moderation.mapper.ts create mode 100644 src/app/features/moderation/models/registry-action-json-api.model.ts create mode 100644 src/app/features/moderation/models/registry-action.model.ts create mode 100644 src/app/features/moderation/models/registry-json-api.model.ts create mode 100644 src/app/features/moderation/models/registry-moderation.model.ts create mode 100644 src/app/features/moderation/services/registry-moderation.service.ts create mode 100644 src/app/features/moderation/store/registry-moderation/index.ts create mode 100644 src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts create mode 100644 src/app/features/moderation/store/registry-moderation/registry-moderation.model.ts create mode 100644 src/app/features/moderation/store/registry-moderation/registry-moderation.selectors.ts create mode 100644 src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts create mode 100644 src/app/shared/pipes/date-ago.pipe.ts delete mode 100644 src/app/shared/pipes/month-year.pipe.spec.ts delete mode 100644 src/app/shared/pipes/wrap-fn.pipe.spec.ts diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts index ef89edfb4..3dd125e13 100644 --- a/src/app/features/moderation/components/index.ts +++ b/src/app/features/moderation/components/index.ts @@ -8,7 +8,9 @@ export { MyReviewingNavigationComponent } from './my-reviewing-navigation/my-rev export { NotificationSettingsComponent } from './notification-settings/notification-settings.component'; export { PreprintModerationSettingsComponent } from './preprint-moderation-settings/preprint-moderation-settings.component'; export { RecentActivityListComponent } from './recent-activity-list/recent-activity-list.component'; +export { RegistryPendingSubmissionsComponent } from './registry-pending-submissions/registry-pending-submissions.component'; export { RegistrySettingsComponent } from './registry-settings/registry-settings.component'; +export { RegistrySubmissionItemComponent } from './registry-submission-item/registry-submission-item.component'; export { RegistrySubmissionsComponent } from './registry-submissions/registry-submissions.component'; export { SubmissionItemComponent } from './submission-item/submission-item.component'; export { SubmissionsListComponent } from './submissions-list/submissions-list.component'; diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html new file mode 100644 index 000000000..a42985576 --- /dev/null +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html @@ -0,0 +1,58 @@ +
+
+ + + +

{{ item.label | translate | titlecase }}

+
+
+
+ +
+ +
+
+ +@if (isLoading()) { + +} @else { + @if (submissions().length) { +
+ @for (item of submissions(); track item.id) { +
+ +
+ } +
+ + @if (totalCount() > pageSize()) { +
+ +
+ } + } @else { +
+

{{ 'moderation.noSubmissions' | translate }}

+
+ } +} diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.scss b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.scss new file mode 100644 index 000000000..6b8291665 --- /dev/null +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.scss @@ -0,0 +1,13 @@ +.submission-container { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; +} + +.submission-item:not(:last-child) { + border-bottom: 1px solid var(--grey-2); +} + +.pending, +.pending-updates { + color: var(--yellow-1); +} diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts new file mode 100644 index 000000000..bb87a80f2 --- /dev/null +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistryPendingSubmissionsComponent } from './registry-pending-submissions.component'; + +describe('RegistryPendingSubmissionsComponent', () => { + let component: RegistryPendingSubmissionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistryPendingSubmissionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistryPendingSubmissionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts new file mode 100644 index 000000000..37df7f19b --- /dev/null +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts @@ -0,0 +1,108 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { PaginatorState } from 'primeng/paginator'; +import { SelectButton } from 'primeng/selectbutton'; + +import { map, of } from 'rxjs'; + +import { TitleCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +import { Primitive } from '@osf/core/helpers'; +import { PENDING_SUBMISSION_REVIEW_OPTIONS, REGISTRY_SORT_OPTIONS } from '@osf/features/moderation/constants'; +import { RegistrySort, SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { + GetRegistrySubmissions, + RegistryModerationSelectors, +} from '@osf/features/moderation/store/registry-moderation'; +import { + CustomPaginatorComponent, + IconComponent, + LoadingSpinnerComponent, + SelectComponent, +} from '@osf/shared/components'; + +import { RegistrySubmissionItemComponent } from '..'; + +@Component({ + selector: 'osf-registry-pending-submissions', + imports: [ + SelectButton, + TranslatePipe, + FormsModule, + SelectComponent, + IconComponent, + TitleCasePipe, + LoadingSpinnerComponent, + RegistrySubmissionItemComponent, + CustomPaginatorComponent, + ], + templateUrl: './registry-pending-submissions.component.html', + styleUrl: './registry-pending-submissions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistryPendingSubmissionsComponent implements OnInit { + readonly submissionReviewOptions = PENDING_SUBMISSION_REVIEW_OPTIONS; + readonly sortOptions = REGISTRY_SORT_OPTIONS; + + private readonly route = inject(ActivatedRoute); + private readonly providerId = toSignal( + this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) + ); + + readonly actions = createDispatchMap({ getRegistrySubmissions: GetRegistrySubmissions }); + + readonly submissions = select(RegistryModerationSelectors.getRegistrySubmissions); + readonly isLoading = select(RegistryModerationSelectors.areRegistrySubmissionLoading); + readonly totalCount = select(RegistryModerationSelectors.getRegistrySubmissionTotalCount); + + readonly currentPage = signal(1); + readonly pageSize = signal(10); + readonly first = signal(0); + readonly selectedSortOption = signal(RegistrySort.RegisteredNewest); + readonly selectedReviewOption = signal(this.submissionReviewOptions[0].value); + + ngOnInit(): void { + this.fetchSubmissions(); + } + + changeReviewStatus(value: SubmissionReviewStatus): void { + this.selectedReviewOption.set(value); + this.resetPagination(); + this.fetchSubmissions(); + } + + changeSort(value: Primitive): void { + this.selectedSortOption.set(value as RegistrySort); + this.fetchSubmissions(); + } + + onPageChange(event: PaginatorState): void { + this.currentPage.set(event.page ? event.page + 1 : 1); + this.first.set(event.first ?? 0); + this.fetchSubmissions(); + } + + private resetPagination(): void { + this.currentPage.set(1); + this.first.set(0); + } + + private fetchSubmissions(): void { + const providerId = this.providerId(); + + if (!providerId) return; + + this.actions.getRegistrySubmissions( + providerId, + this.selectedReviewOption(), + this.currentPage(), + this.selectedSortOption() + ); + } +} diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html new file mode 100644 index 000000000..8cb14b9c8 --- /dev/null +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html @@ -0,0 +1,46 @@ +
+ + +
+ + {{ submission().title }} + + + @if (submission().public && !submission().embargoEndDate) { +

{{ 'registry.overview.statuses.accepted.text' | translate }}

+ } + + @if (submission().embargoEndDate) { +

{{ 'moderation.registryEmbargoedWithEndDate' | translate }}

+ } + + @for (action of showAll ? submission().actions : submission().actions.slice(0, limitValue); track $index) { +
+ {{ reviewStatusIcon[submission().reviewsState].description | translate }} + {{ action.dateModified | dateAgo }} + {{ 'moderation.submissionReview.by' | translate }} + {{ action.creator.name }} + {{ 'moderation.withNoEmbargo' | translate }} + @if (action.comment.length) { + - {{ action.comment }} + } +
+ } + + @if (submission().actions.length > 1) { + + } +
+
diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss new file mode 100644 index 000000000..eb051b7b3 --- /dev/null +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss @@ -0,0 +1,24 @@ +.pending { + color: var(--yellow-1); +} + +.accepted { + color: var(--green-1); +} + +.rejected { + color: var(--red-1); +} + +.withdrawn { + color: var(--dark-blue-1); +} + +.submission-link { + color: var(--dark-blue-1); +} + +.link-btn { + --p-button-padding-y: 0; + --p-button-padding-x: 0; +} diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts new file mode 100644 index 000000000..98c9a2bec --- /dev/null +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistrySubmissionItemComponent } from './registry-submission-item.component'; + +describe('RegistrySubmissionItemComponent', () => { + let component: RegistrySubmissionItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistrySubmissionItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistrySubmissionItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts new file mode 100644 index 000000000..fed67e952 --- /dev/null +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts @@ -0,0 +1,33 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { IconComponent } from '@osf/shared/components'; +import { DateAgoPipe } from '@osf/shared/pipes'; + +import { ReviewStatusIcon } from '../../constants'; +import { RegistryModeration } from '../../models'; + +@Component({ + selector: 'osf-registry-submission-item', + imports: [IconComponent, DateAgoPipe, Button, TranslatePipe, RouterLink], + templateUrl: './registry-submission-item.component.html', + styleUrl: './registry-submission-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RegistrySubmissionItemComponent { + submission = input.required(); + selected = output(); + + readonly reviewStatusIcon = ReviewStatusIcon; + + limitValue = 1; + showAll = false; + + toggleHistory() { + this.showAll = !this.showAll; + } +} diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html index 4e63c0b8a..030602784 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html @@ -5,12 +5,12 @@ [options]="submissionReviewOptions" [ngModel]="selectedReviewOption" (ngModelChange)="changeReviewStatus($event)" + [disabled]="isLoading()" optionLabel="label" optionValue="value" > -

{{ totalCount }}

{{ item.label | translate | titlecase }}

@@ -23,8 +23,36 @@ [fullWidth]="true" [(selectedValue)]="selectedSortOption" (changeValue)="changeSort($event)" + [disabled]="isLoading()" > - +@if (isLoading()) { + +} @else { + @if (submissions().length) { +
+ @for (item of submissions(); track $index) { +
+ +
+ } +
+ + @if (totalCount() > pageSize()) { +
+ +
+ } @else { +
+

{{ 'moderation.noSubmissions' | translate }}

+
+ } + } +} diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss index baf8accf8..d1909e9e4 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss @@ -1,3 +1,12 @@ +.submission-container { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; +} + +.submission-item:not(:last-child) { + border-bottom: 1px solid var(--grey-2); +} + .pending { color: var(--yellow-1); } @@ -9,7 +18,3 @@ .rejected { color: var(--red-1); } - -.withdrawn { - color: var(--dark-blue-1); -} diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts index 9c1102cbd..6beffeeb0 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts @@ -1,19 +1,30 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; +import { PaginatorState } from 'primeng/paginator'; import { SelectButton } from 'primeng/selectbutton'; +import { map, of } from 'rxjs'; + import { TitleCasePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; import { Primitive } from '@osf/core/helpers'; -import { IconComponent, SelectComponent } from '@osf/shared/components'; -import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; +import { + CustomPaginatorComponent, + IconComponent, + LoadingSpinnerComponent, + SelectComponent, +} from '@osf/shared/components'; -import { SUBMITTED_SUBMISSION_REVIEW_OPTIONS } from '../../constants'; -import { SubmissionReviewStatus } from '../../enums'; -import { SubmissionsListComponent } from '../submissions-list/submissions-list.component'; -import { pubicReviews } from '../test-data'; +import { REGISTRY_SORT_OPTIONS, SUBMITTED_SUBMISSION_REVIEW_OPTIONS } from '../../constants'; +import { RegistrySort, SubmissionReviewStatus } from '../../enums'; +import { GetRegistrySubmissions, RegistryModerationSelectors } from '../../store/registry-moderation'; +import { RegistrySubmissionItemComponent } from '..'; @Component({ selector: 'osf-registry-submissions', @@ -22,30 +33,74 @@ import { pubicReviews } from '../test-data'; TranslatePipe, FormsModule, SelectComponent, - SubmissionsListComponent, IconComponent, TitleCasePipe, + LoadingSpinnerComponent, + RegistrySubmissionItemComponent, + CustomPaginatorComponent, ], templateUrl: './registry-submissions.component.html', styleUrl: './registry-submissions.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RegistrySubmissionsComponent { +export class RegistrySubmissionsComponent implements OnInit { readonly submissionReviewOptions = SUBMITTED_SUBMISSION_REVIEW_OPTIONS; + readonly sortOptions = REGISTRY_SORT_OPTIONS; + + private readonly route = inject(ActivatedRoute); + private readonly providerId = toSignal( + this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) + ); + + readonly actions = createDispatchMap({ getRegistrySubmissions: GetRegistrySubmissions }); + + readonly submissions = select(RegistryModerationSelectors.getRegistrySubmissions); + readonly isLoading = select(RegistryModerationSelectors.areRegistrySubmissionLoading); + readonly totalCount = select(RegistryModerationSelectors.getRegistrySubmissionTotalCount); + + readonly currentPage = signal(1); + readonly pageSize = signal(10); + readonly first = signal(0); + readonly selectedSortOption = signal(RegistrySort.RegisteredNewest); + readonly selectedReviewOption = signal(this.submissionReviewOptions[0].value); + + readonly actualStatus = computed(() => + this.selectedReviewOption() === SubmissionReviewStatus.Public + ? SubmissionReviewStatus.Accepted + : this.selectedReviewOption() + ); - sortOptions = ALL_SORT_OPTIONS; - selectedSortOption = signal(null); - selectedReviewOption = this.submissionReviewOptions[0].value; + ngOnInit(): void { + this.fetchSubmissions(); + } - totalCount = 5; + changeReviewStatus(value: SubmissionReviewStatus): void { + this.selectedReviewOption.set(value); + this.resetPagination(); + this.fetchSubmissions(); + } - submissions = pubicReviews; + changeSort(value: Primitive): void { + this.selectedSortOption.set(value as RegistrySort); + this.fetchSubmissions(); + } - changeReviewStatus(value: SubmissionReviewStatus) { - console.log(value); + onPageChange(event: PaginatorState): void { + this.currentPage.set(event.page ? event.page + 1 : 1); + this.first.set(event.first ?? 0); + this.fetchSubmissions(); } - changeSort(value: Primitive) { - console.log(value); + private resetPagination(): void { + this.currentPage.set(1); + this.first.set(0); + } + + private fetchSubmissions(): void { + const providerId = this.providerId(); + + if (!providerId) return; + + this.actions.getRegistrySubmissions(providerId, this.actualStatus(), this.currentPage(), this.selectedSortOption()); } } diff --git a/src/app/features/moderation/constants/index.ts b/src/app/features/moderation/constants/index.ts index 32498e13f..7be0752a4 100644 --- a/src/app/features/moderation/constants/index.ts +++ b/src/app/features/moderation/constants/index.ts @@ -3,5 +3,6 @@ export * from './my-preprint-reviewing.const'; export * from './preprint-moderation-tabs.const'; export * from './preprint-settings-sections.const'; export * from './registry-moderation-tabs.const'; +export * from './registry-sort-options.const'; export * from './submission.const'; export * from './upload-limits.const'; diff --git a/src/app/features/moderation/constants/registry-sort-options.const.ts b/src/app/features/moderation/constants/registry-sort-options.const.ts new file mode 100644 index 000000000..59479ca8f --- /dev/null +++ b/src/app/features/moderation/constants/registry-sort-options.const.ts @@ -0,0 +1,22 @@ +import { CustomOption } from '@osf/shared/models'; + +import { RegistrySort } from '../enums'; + +export const REGISTRY_SORT_OPTIONS: CustomOption[] = [ + { + value: RegistrySort.TitleAZ, + label: 'moderation.registrySortOption.titleAZ', + }, + { + value: RegistrySort.TitleZA, + label: 'moderation.registrySortOption.titleZA', + }, + { + value: RegistrySort.RegisteredNewest, + label: 'moderation.registrySortOption.registeredOldest', + }, + { + value: RegistrySort.RegisteredOldest, + label: 'moderation.registrySortOption.registeredNewest', + }, +]; diff --git a/src/app/features/moderation/constants/submission.const.ts b/src/app/features/moderation/constants/submission.const.ts index 0bdf6f298..dca2faf16 100644 --- a/src/app/features/moderation/constants/submission.const.ts +++ b/src/app/features/moderation/constants/submission.const.ts @@ -26,7 +26,7 @@ export const SUBMISSION_REVIEW_OPTIONS = [ export const SUBMITTED_SUBMISSION_REVIEW_OPTIONS = [ { value: SubmissionReviewStatus.Public, - icon: 'fas fa-lock', + icon: 'fas fa-circle-check', label: 'moderation.submissionReviewStatus.public', }, { @@ -46,29 +46,67 @@ export const SUBMITTED_SUBMISSION_REVIEW_OPTIONS = [ }, ]; -export const ReviewStatusIcon: Record = { +export const PENDING_SUBMISSION_REVIEW_OPTIONS = [ + { + value: SubmissionReviewStatus.Pending, + icon: 'fas fa-hourglass', + label: 'moderation.submissionReviewStatus.pending', + description: 'moderation.registrySubmitted', + }, + { + value: SubmissionReviewStatus.PendingUpdates, + icon: 'fas fa-hourglass', + label: 'moderation.submissionReviewStatus.pendingUpdates', + }, + { + value: SubmissionReviewStatus.PendingWithdrawal, + icon: 'fas fa-circle-minus', + label: 'moderation.submissionReviewStatus.pendingWithdrawal', + }, +]; + +export const ReviewStatusIcon: Record< + SubmissionReviewStatus | string, + { value: string; icon: string; description: string } +> = { [SubmissionReviewStatus.Pending]: { value: SubmissionReviewStatus.Pending, icon: 'fas fa-hourglass', + description: 'moderation.registrySubmitted', }, [SubmissionReviewStatus.Accepted]: { value: SubmissionReviewStatus.Accepted, icon: 'fas fa-circle-check', + description: 'moderation.registryAccepted', }, [SubmissionReviewStatus.Rejected]: { value: SubmissionReviewStatus.Rejected, icon: 'fas fa-circle-xmark', + description: 'moderation.registryRejected', }, [SubmissionReviewStatus.Withdrawn]: { value: SubmissionReviewStatus.Withdrawn, icon: 'fas fa-circle-minus', + description: 'moderation.registryWithdrawal', }, [SubmissionReviewStatus.Public]: { value: SubmissionReviewStatus.Public, icon: 'fas fa-lock', + description: 'moderation.registryAccepted', }, [SubmissionReviewStatus.Embargo]: { value: SubmissionReviewStatus.Embargo, icon: 'fas fa-lock-open', + description: 'moderation.registryAccepted', + }, + [SubmissionReviewStatus.PendingWithdrawal]: { + value: SubmissionReviewStatus.PendingWithdrawal, + icon: 'fas fa-circle-minus', + description: 'moderation.registrySubmitted', + }, + [SubmissionReviewStatus.PendingUpdates]: { + value: SubmissionReviewStatus.PendingUpdates, + icon: 'fas fa-hourglass', + description: 'moderation.registrySubmitted', }, }; diff --git a/src/app/features/moderation/enums/index.ts b/src/app/features/moderation/enums/index.ts index 3890b7661..a3d0d5979 100644 --- a/src/app/features/moderation/enums/index.ts +++ b/src/app/features/moderation/enums/index.ts @@ -4,5 +4,6 @@ export * from './moderation-type.enum'; export * from './moderator-permission.enum'; export * from './preprint-moderation-tab.enum'; export * from './registry-moderation-tab.enum'; +export * from './registry-sort.enum'; export * from './settings-section-control.enum'; export * from './submission-review-status.enum'; diff --git a/src/app/features/moderation/enums/registry-sort.enum.ts b/src/app/features/moderation/enums/registry-sort.enum.ts new file mode 100644 index 000000000..d8645c4e4 --- /dev/null +++ b/src/app/features/moderation/enums/registry-sort.enum.ts @@ -0,0 +1,6 @@ +export enum RegistrySort { + TitleAZ = 'title', + TitleZA = '-title', + RegisteredOldest = 'date_registered', + RegisteredNewest = '-date_registered', +} diff --git a/src/app/features/moderation/enums/submission-review-status.enum.ts b/src/app/features/moderation/enums/submission-review-status.enum.ts index 990e0d2a3..586d69a14 100644 --- a/src/app/features/moderation/enums/submission-review-status.enum.ts +++ b/src/app/features/moderation/enums/submission-review-status.enum.ts @@ -5,4 +5,6 @@ export enum SubmissionReviewStatus { Withdrawn = 'withdrawn', Public = 'public', Embargo = 'embargo', + PendingUpdates = 'pending-updates', + PendingWithdrawal = 'pending-withdrawal', } diff --git a/src/app/features/moderation/mappers/index.ts b/src/app/features/moderation/mappers/index.ts index 90bd110d9..4a68121c2 100644 --- a/src/app/features/moderation/mappers/index.ts +++ b/src/app/features/moderation/mappers/index.ts @@ -1,2 +1,3 @@ export * from './moderation.mapper'; export * from './preprint-moderation.mapper'; +export * from './registry-moderation.mapper'; diff --git a/src/app/features/moderation/mappers/registry-moderation.mapper.ts b/src/app/features/moderation/mappers/registry-moderation.mapper.ts new file mode 100644 index 000000000..6947c989f --- /dev/null +++ b/src/app/features/moderation/mappers/registry-moderation.mapper.ts @@ -0,0 +1,44 @@ +import { PaginatedData } from '@osf/shared/models'; + +import { + RegistryAction, + RegistryActionsDataJsonApi, + RegistryDataJsonApi, + RegistryModeration, + RegistryResponseJsonApi, +} from '../models'; + +export class RegistryModerationMapper { + static fromResponse(response: RegistryDataJsonApi): RegistryModeration { + return { + id: response.id, + title: response.attributes.title, + reviewsState: response.attributes.reviews_state, + public: response.attributes.public, + embargoed: response.attributes.embargoed, + embargoEndDate: response.attributes.embargo_end_date, + actions: [], + }; + } + + static fromResponseWithPagination(response: RegistryResponseJsonApi): PaginatedData { + return { + data: response.data.map((x) => this.fromResponse(x)), + totalCount: response.links.meta.total, + }; + } + + static fromActionResponse(response: RegistryActionsDataJsonApi): RegistryAction { + return { + id: response.id, + fromState: response.attributes.from_state, + toState: response.attributes.to_state, + dateModified: response.attributes.date_modified, + comment: response.attributes.comment, + creator: { + id: response.embeds.creator.data.id, + name: response.embeds.creator.data.attributes.full_name, + }, + }; + } +} diff --git a/src/app/features/moderation/models/index.ts b/src/app/features/moderation/models/index.ts index 38e717b41..ab65f511d 100644 --- a/src/app/features/moderation/models/index.ts +++ b/src/app/features/moderation/models/index.ts @@ -7,4 +7,8 @@ export * from './preprint-provider-moderation-info.model'; export * from './preprint-related-count-json-api.model'; export * from './preprint-review-action.model'; export * from './preprint-review-action-json-api.model'; +export * from './registry-action.model'; +export * from './registry-action-json-api.model'; +export * from './registry-json-api.model'; +export * from './registry-moderation.model'; export * from './submission.model'; diff --git a/src/app/features/moderation/models/registry-action-json-api.model.ts b/src/app/features/moderation/models/registry-action-json-api.model.ts new file mode 100644 index 000000000..a62bfa493 --- /dev/null +++ b/src/app/features/moderation/models/registry-action-json-api.model.ts @@ -0,0 +1,36 @@ +import { JsonApiResponse } from '@osf/core/models'; + +export type RegistryActionsResponseJsonApi = JsonApiResponse; + +export interface RegistryActionsDataJsonApi { + id: string; + attributes: RegistryActionAttributesJsonApi; + embeds: RegistryActionEmbedsJsonApi; +} + +interface RegistryActionAttributesJsonApi { + auto: boolean; + comment: string; + date_created: string; + date_modified: string; + from_state: string; + to_state: string; + trigger: string; + visible: true; +} + +interface RegistryActionEmbedsJsonApi { + creator: { + data: UserModelJsonApi; + }; +} + +interface UserModelJsonApi { + id: string; + type: 'users'; + attributes: UserAttributesJsonApi; +} + +interface UserAttributesJsonApi { + full_name: string; +} diff --git a/src/app/features/moderation/models/registry-action.model.ts b/src/app/features/moderation/models/registry-action.model.ts new file mode 100644 index 000000000..476977c58 --- /dev/null +++ b/src/app/features/moderation/models/registry-action.model.ts @@ -0,0 +1,10 @@ +import { IdName } from '@osf/shared/models'; + +export interface RegistryAction { + id: string; + fromState: string; + toState: string; + dateModified: string; + creator: IdName; + comment: string; +} diff --git a/src/app/features/moderation/models/registry-json-api.model.ts b/src/app/features/moderation/models/registry-json-api.model.ts new file mode 100644 index 000000000..a026b5d25 --- /dev/null +++ b/src/app/features/moderation/models/registry-json-api.model.ts @@ -0,0 +1,17 @@ +import { JsonApiResponseWithPaging } from '@osf/core/models'; + +export type RegistryResponseJsonApi = JsonApiResponseWithPaging; + +export interface RegistryDataJsonApi { + id: string; + attributes: RegistryAttributesJsonApi; +} + +export interface RegistryAttributesJsonApi { + id: string; + title: string; + reviews_state: string; + public: boolean; + embargoed: boolean; + embargo_end_date: string; +} diff --git a/src/app/features/moderation/models/registry-moderation.model.ts b/src/app/features/moderation/models/registry-moderation.model.ts new file mode 100644 index 000000000..3f56b3891 --- /dev/null +++ b/src/app/features/moderation/models/registry-moderation.model.ts @@ -0,0 +1,11 @@ +import { RegistryAction } from './registry-action.model'; + +export interface RegistryModeration { + id: string; + title: string; + reviewsState: string; + public: boolean; + embargoed: boolean; + embargoEndDate?: string; + actions: RegistryAction[]; +} diff --git a/src/app/features/moderation/registry-moderation.routes.ts b/src/app/features/moderation/registry-moderation.routes.ts index d78fded4c..6b2e40a64 100644 --- a/src/app/features/moderation/registry-moderation.routes.ts +++ b/src/app/features/moderation/registry-moderation.routes.ts @@ -5,6 +5,7 @@ import { Routes } from '@angular/router'; import { ResourceType } from '@osf/shared/enums'; import { ModeratorsState } from './store/moderation'; +import { RegistryModerationState } from './store/registry-moderation'; import { RegistryModerationTab } from './enums'; export const registryModerationRoutes: Routes = [ @@ -14,6 +15,7 @@ export const registryModerationRoutes: Routes = [ import('@osf/features/moderation/pages/registries-moderation/registries-moderation.component').then( (m) => m.RegistriesModerationComponent ), + providers: [provideStates([RegistryModerationState])], children: [ { path: '', @@ -31,8 +33,8 @@ export const registryModerationRoutes: Routes = [ { path: 'pending', loadComponent: () => - import('./components/collection-moderation-submissions/collection-moderation-submissions.component').then( - (m) => m.CollectionModerationSubmissionsComponent + import('./components/registry-pending-submissions/registry-pending-submissions.component').then( + (m) => m.RegistryPendingSubmissionsComponent ), data: { tab: RegistryModerationTab.Pending }, }, diff --git a/src/app/features/moderation/services/index.ts b/src/app/features/moderation/services/index.ts index 123a65ff3..9ef953442 100644 --- a/src/app/features/moderation/services/index.ts +++ b/src/app/features/moderation/services/index.ts @@ -1,2 +1,3 @@ export { ModeratorsService } from './moderators.service'; export { PreprintModerationService } from './preprint-moderation.service'; +export { RegistryModerationService } from './registry-moderation.service'; diff --git a/src/app/features/moderation/services/registry-moderation.service.ts b/src/app/features/moderation/services/registry-moderation.service.ts new file mode 100644 index 000000000..b2a9543ee --- /dev/null +++ b/src/app/features/moderation/services/registry-moderation.service.ts @@ -0,0 +1,45 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; +import { PaginatedData } from '@osf/shared/models'; + +import { RegistrySort, SubmissionReviewStatus } from '../enums'; +import { RegistryModerationMapper } from '../mappers'; +import { RegistryAction, RegistryActionsResponseJsonApi, RegistryModeration, RegistryResponseJsonApi } from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class RegistryModerationService { + private readonly jsonApiService = inject(JsonApiService); + + getRegistrySubmissions( + provider: string, + status: string, + page = 1, + sort = RegistrySort.RegisteredNewest + ): Observable> { + const filters = + status === SubmissionReviewStatus.PendingUpdates + ? `filter[reviews_state]=embargo,accepted&filter[revision_state]=pending_moderation` + : `filter[reviews_state]=${status}`; + + const baseUrl = `${environment.apiUrl}/providers/registrations/${provider}/registrations/?page=${page}&page[size]=10&${filters}&sort=${sort}`; + + return this.jsonApiService + .get(baseUrl) + .pipe(map((response) => RegistryModerationMapper.fromResponseWithPagination(response))); + } + + getRegistrySubmissionHistory(id: string): Observable { + const baseUrl = `${environment.apiUrl}/registrations/${id}/actions/`; + + return this.jsonApiService + .get(baseUrl) + .pipe(map((response) => response.data.map((x) => RegistryModerationMapper.fromActionResponse(x)))); + } +} diff --git a/src/app/features/moderation/store/registry-moderation/index.ts b/src/app/features/moderation/store/registry-moderation/index.ts new file mode 100644 index 000000000..70f3de9ad --- /dev/null +++ b/src/app/features/moderation/store/registry-moderation/index.ts @@ -0,0 +1,4 @@ +export * from './registry-moderation.actions'; +export * from './registry-moderation.model'; +export * from './registry-moderation.selectors'; +export * from './registry-moderation.state'; diff --git a/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts b/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts new file mode 100644 index 000000000..3f7026c0a --- /dev/null +++ b/src/app/features/moderation/store/registry-moderation/registry-moderation.actions.ts @@ -0,0 +1,14 @@ +import { RegistrySort } from '../../enums'; + +const ACTION_SCOPE = '[Registry Moderation]'; + +export class GetRegistrySubmissions { + static readonly type = `${ACTION_SCOPE} Get Registry Submissions`; + + constructor( + public provider: string, + public status: string, + public page?: number, + public sort?: RegistrySort + ) {} +} diff --git a/src/app/features/moderation/store/registry-moderation/registry-moderation.model.ts b/src/app/features/moderation/store/registry-moderation/registry-moderation.model.ts new file mode 100644 index 000000000..4e17fb03d --- /dev/null +++ b/src/app/features/moderation/store/registry-moderation/registry-moderation.model.ts @@ -0,0 +1,16 @@ +import { AsyncStateWithTotalCount } from '@osf/shared/models'; + +import { RegistryModeration } from '../../models'; + +export interface RegistryModerationStateModel { + submissions: AsyncStateWithTotalCount; +} + +export const REGISTRY_MODERATION_STATE_DEFAULTS: RegistryModerationStateModel = { + submissions: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + }, +}; diff --git a/src/app/features/moderation/store/registry-moderation/registry-moderation.selectors.ts b/src/app/features/moderation/store/registry-moderation/registry-moderation.selectors.ts new file mode 100644 index 000000000..5861436b2 --- /dev/null +++ b/src/app/features/moderation/store/registry-moderation/registry-moderation.selectors.ts @@ -0,0 +1,21 @@ +import { Selector } from '@ngxs/store'; + +import { RegistryModerationStateModel } from './registry-moderation.model'; +import { RegistryModerationState } from './registry-moderation.state'; + +export class RegistryModerationSelectors { + @Selector([RegistryModerationState]) + static getRegistrySubmissions(state: RegistryModerationStateModel) { + return state.submissions.data; + } + + @Selector([RegistryModerationState]) + static areRegistrySubmissionLoading(state: RegistryModerationStateModel) { + return state.submissions.isLoading; + } + + @Selector([RegistryModerationState]) + static getRegistrySubmissionTotalCount(state: RegistryModerationStateModel) { + return state.submissions.totalCount; + } +} diff --git a/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts b/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts new file mode 100644 index 000000000..7a970990a --- /dev/null +++ b/src/app/features/moderation/store/registry-moderation/registry-moderation.state.ts @@ -0,0 +1,69 @@ +import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; + +import { catchError, forkJoin, map, of, switchMap, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@osf/core/handlers'; +import { PaginatedData } from '@osf/shared/models'; + +import { RegistryModeration } from '../../models'; +import { RegistryModerationService } from '../../services'; + +import { GetRegistrySubmissions } from './registry-moderation.actions'; +import { REGISTRY_MODERATION_STATE_DEFAULTS, RegistryModerationStateModel } from './registry-moderation.model'; + +@State({ + name: 'registryModeration', + defaults: REGISTRY_MODERATION_STATE_DEFAULTS, +}) +@Injectable() +export class RegistryModerationState { + private readonly registryModerationService = inject(RegistryModerationService); + + @Action(GetRegistrySubmissions) + getRegistrySubmissions( + ctx: StateContext, + { provider, status, page, sort }: GetRegistrySubmissions + ) { + ctx.setState(patch({ submissions: patch({ isLoading: true }) })); + + return this.registryModerationService.getRegistrySubmissions(provider, status, page, sort).pipe( + switchMap((res) => { + if (!res.data.length) { + return of({ + data: [], + totalCount: res.totalCount, + }); + } + + const actionRequests = res.data.map((item) => + this.registryModerationService.getRegistrySubmissionHistory(item.id) + ); + + return forkJoin(actionRequests).pipe( + map( + (actions) => + ({ + data: res.data.map((item, i) => ({ ...item, actions: actions[i] })), + totalCount: res.totalCount, + }) as PaginatedData + ) + ); + }), + tap((res) => { + ctx.setState( + patch({ + submissions: patch({ + data: res.data, + isLoading: false, + totalCount: res.totalCount, + }), + }) + ); + }), + catchError((error) => handleSectionError(ctx, 'submissions', error)) + ); + } +} diff --git a/src/app/shared/components/bar-chart/bar-chart.component.html b/src/app/shared/components/bar-chart/bar-chart.component.html index f9c424f91..c3ebc58a0 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.html +++ b/src/app/shared/components/bar-chart/bar-chart.component.html @@ -7,7 +7,7 @@

{{ title() | translate }}

} @else {
- +
@if (showExpandedSection()) { diff --git a/src/app/shared/components/line-chart/line-chart.component.html b/src/app/shared/components/line-chart/line-chart.component.html index 961d8bf4d..60ecd8df0 100644 --- a/src/app/shared/components/line-chart/line-chart.component.html +++ b/src/app/shared/components/line-chart/line-chart.component.html @@ -4,6 +4,6 @@

{{ title() | translate }}

} @else {
- +
} diff --git a/src/app/shared/components/select/select.component.html b/src/app/shared/components/select/select.component.html index d89bf0914..f26b64785 100644 --- a/src/app/shared/components/select/select.component.html +++ b/src/app/shared/components/select/select.component.html @@ -6,6 +6,7 @@ optionValue="value" [appendTo]="appendTo()" [placeholder]="placeholder() | translate" + [disabled]="disabled()" [(ngModel)]="selectedValue" (ngModelChange)="changeValue.emit($event)" > diff --git a/src/app/shared/components/select/select.component.ts b/src/app/shared/components/select/select.component.ts index 035383241..a326ef712 100644 --- a/src/app/shared/components/select/select.component.ts +++ b/src/app/shared/components/select/select.component.ts @@ -22,6 +22,7 @@ export class SelectComponent { appendTo = input(null); fullWidth = input(false); noBorder = input(false); + disabled = input(false); changeValue = output(); } diff --git a/src/app/shared/pipes/date-ago.pipe.ts b/src/app/shared/pipes/date-ago.pipe.ts new file mode 100644 index 000000000..5b8343d92 --- /dev/null +++ b/src/app/shared/pipes/date-ago.pipe.ts @@ -0,0 +1,35 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'dateAgo', +}) +export class DateAgoPipe implements PipeTransform { + private readonly formatter = new Intl.RelativeTimeFormat('en', { + numeric: 'auto', + style: 'long', + }); + + private readonly units: [Intl.RelativeTimeFormatUnit, number][] = [ + ['year', 31536000], + ['month', 2592000], + ['day', 86400], + ['hour', 3600], + ['minute', 60], + ['second', 1], + ]; + + transform(value: string | Date | number): string { + const date = new Date(value); + if (isNaN(date.getTime())) return 'Invalid date'; + + const seconds = (date.getTime() - Date.now()) / 1000; + + for (const [unit, secondsInUnit] of this.units) { + if (Math.abs(seconds) >= secondsInUnit || unit === 'second') { + return this.formatter.format(Math.round(seconds / secondsInUnit), unit); + } + } + + return 'just now'; + } +} diff --git a/src/app/shared/pipes/index.ts b/src/app/shared/pipes/index.ts index b9ee0e97c..1d8de41af 100644 --- a/src/app/shared/pipes/index.ts +++ b/src/app/shared/pipes/index.ts @@ -1,4 +1,5 @@ export { CitationFormatPipe } from './citation-format.pipe'; +export { DateAgoPipe } from './date-ago.pipe'; export { DecodeHtmlPipe } from './decode-html.pipe'; export { FileSizePipe } from './file-size.pipe'; export { InterpolatePipe } from './interpolate.pipe'; diff --git a/src/app/shared/pipes/month-year.pipe.spec.ts b/src/app/shared/pipes/month-year.pipe.spec.ts deleted file mode 100644 index 2839cf5af..000000000 --- a/src/app/shared/pipes/month-year.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MonthYearPipe } from './month-year.pipe'; - -describe('MonthYearPipe', () => { - it('create an instance', () => { - const pipe = new MonthYearPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/app/shared/pipes/wrap-fn.pipe.spec.ts b/src/app/shared/pipes/wrap-fn.pipe.spec.ts deleted file mode 100644 index e229f8a6e..000000000 --- a/src/app/shared/pipes/wrap-fn.pipe.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { WrapFnPipe } from './wrap-fn.pipe'; - -describe('WrapFnPipe', () => { - it('create an instance', () => { - const pipe = new WrapFnPipe(); - expect(pipe).toBeTruthy(); - }); -}); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index af0d390bb..7a4a1d483 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -39,7 +39,8 @@ "submit": "Submit", "view": "View", "review": "Review", - "upload": "Upload" + "upload": "Upload", + "hide": "Hide" }, "search": { "title": "Search", @@ -1124,6 +1125,7 @@ "submitted": "Submitted", "pending": "Pending", "submissions": "Submissions", + "noSubmissions": "No submissions.", "withdrawalRequest": "Withdrawal Request", "notifications": "Notifications", "settingsMessage": "To configure your notification preferences visit your", @@ -1154,7 +1156,9 @@ "rejected": "Rejected", "withdrawn": "Withdrawn", "public": "Public", - "embargo": "Embargo" + "embargo": "Embargo", + "pendingUpdates": "Pending Updates", + "pendingWithdrawal": "Pending Withdrawal" }, "submissionReview": { "submitted": "Submitted", @@ -1194,6 +1198,19 @@ "named": "Named Comments", "namedDescription": "All comments will be visible to the contributors of the submission and the moderator's osf profile name will be displayed." } + }, + "registryAccepted": "Registration submission", + "registrySubmitted": "Registration submitted", + "registryWithdrawal": "Registration withdrawal request accepted", + "registryRejected": "Registration submission rejected", + "registryEmbargoedWithEndDate": "Embargoed Registration with an end date of ", + "withNoEmbargo": "with no embargo", + "showHistory": "Show history", + "registrySortOption": { + "titleAZ": "Title: A-Z", + "titleZA": "Title: Z-A", + "registeredOldest": "Date: oldest to newest", + "registeredNewest": "Date: newest to oldest" } }, "settings": { From 58749de2fa7ca6218077bf202cde5c0f89411b66 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 29 Jul 2025 11:03:18 +0300 Subject: [PATCH 2/4] feat(registries-submissions): fixed labels for different states --- .../registry-pending-submissions.component.html | 5 ++++- .../registry-submission-item.component.html | 15 ++++++++------- .../registry-submission-item.component.scss | 3 ++- .../registry-submission-item.component.ts | 6 +++++- .../registry-submissions.component.html | 15 +++++++++------ src/app/features/moderation/constants/index.ts | 1 + .../constants/registry-action-labels.const.ts | 9 +++++++++ .../moderation/constants/submission.const.ts | 13 +------------ src/app/features/moderation/enums/index.ts | 1 + .../enums/registry-action-state.enum.ts | 7 +++++++ src/assets/i18n/en.json | 5 +++-- 11 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 src/app/features/moderation/constants/registry-action-labels.const.ts create mode 100644 src/app/features/moderation/enums/registry-action-state.enum.ts diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html index a42985576..bf9e01f67 100644 --- a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html @@ -35,7 +35,10 @@
@for (item of submissions(); track item.id) {
- +
}
diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html index 8cb14b9c8..0cf5733cc 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html @@ -1,8 +1,5 @@
- +
- {{ reviewStatusIcon[submission().reviewsState].description | translate }} +
+ {{ registryActionLabel[action.toState] | translate }} {{ action.dateModified | dateAgo }} {{ 'moderation.submissionReview.by' | translate }} {{ action.creator.name }} - {{ 'moderation.withNoEmbargo' | translate }} + + @if (action.toState === registryActionState.Accepted) { + {{ 'moderation.withNoEmbargo' | translate }} + } + @if (action.comment.length) { - {{ action.comment }} } diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss index eb051b7b3..a6103ccdc 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss @@ -1,4 +1,5 @@ -.pending { +.pending, +.pending-updates { color: var(--yellow-1); } diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts index fed67e952..c8741789c 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts @@ -8,7 +8,8 @@ import { RouterLink } from '@angular/router'; import { IconComponent } from '@osf/shared/components'; import { DateAgoPipe } from '@osf/shared/pipes'; -import { ReviewStatusIcon } from '../../constants'; +import { REGISTRY_ACTION_LABEL, ReviewStatusIcon } from '../../constants'; +import { RegistryActionState, SubmissionReviewStatus } from '../../enums'; import { RegistryModeration } from '../../models'; @Component({ @@ -19,10 +20,13 @@ import { RegistryModeration } from '../../models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistrySubmissionItemComponent { + status = input.required(); submission = input.required(); selected = output(); readonly reviewStatusIcon = ReviewStatusIcon; + readonly registryActionLabel = REGISTRY_ACTION_LABEL; + readonly registryActionState = RegistryActionState; limitValue = 1; showAll = false; diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html index 030602784..3de4a6649 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html @@ -3,7 +3,7 @@ @for (item of submissions(); track $index) {
- +
}
@@ -49,10 +52,10 @@ (pageChanged)="onPageChange($event)" >
- } @else { -
-

{{ 'moderation.noSubmissions' | translate }}

-
} + } @else { +
+

{{ 'moderation.noSubmissions' | translate }}

+
} } diff --git a/src/app/features/moderation/constants/index.ts b/src/app/features/moderation/constants/index.ts index 7be0752a4..44e02f8aa 100644 --- a/src/app/features/moderation/constants/index.ts +++ b/src/app/features/moderation/constants/index.ts @@ -2,6 +2,7 @@ export * from './collection-moderation-tabs.const'; export * from './my-preprint-reviewing.const'; export * from './preprint-moderation-tabs.const'; export * from './preprint-settings-sections.const'; +export * from './registry-action-labels.const'; export * from './registry-moderation-tabs.const'; export * from './registry-sort-options.const'; export * from './submission.const'; diff --git a/src/app/features/moderation/constants/registry-action-labels.const.ts b/src/app/features/moderation/constants/registry-action-labels.const.ts new file mode 100644 index 000000000..3a68dc74d --- /dev/null +++ b/src/app/features/moderation/constants/registry-action-labels.const.ts @@ -0,0 +1,9 @@ +import { RegistryActionState } from '../enums'; + +export const REGISTRY_ACTION_LABEL: Record = { + [RegistryActionState.Accepted]: 'moderation.registryAccepted', + [RegistryActionState.Pending]: 'moderation.registrySubmitted', + [RegistryActionState.PendingWithdraw]: 'moderation.registryWithdrawalRequested', + [RegistryActionState.Withdrawn]: 'moderation.registryWithdrawalAccepted', + [RegistryActionState.Rejected]: 'moderation.registryRejected', +}; diff --git a/src/app/features/moderation/constants/submission.const.ts b/src/app/features/moderation/constants/submission.const.ts index dca2faf16..eceefebfa 100644 --- a/src/app/features/moderation/constants/submission.const.ts +++ b/src/app/features/moderation/constants/submission.const.ts @@ -65,48 +65,37 @@ export const PENDING_SUBMISSION_REVIEW_OPTIONS = [ }, ]; -export const ReviewStatusIcon: Record< - SubmissionReviewStatus | string, - { value: string; icon: string; description: string } -> = { +export const ReviewStatusIcon: Record = { [SubmissionReviewStatus.Pending]: { value: SubmissionReviewStatus.Pending, icon: 'fas fa-hourglass', - description: 'moderation.registrySubmitted', }, [SubmissionReviewStatus.Accepted]: { value: SubmissionReviewStatus.Accepted, icon: 'fas fa-circle-check', - description: 'moderation.registryAccepted', }, [SubmissionReviewStatus.Rejected]: { value: SubmissionReviewStatus.Rejected, icon: 'fas fa-circle-xmark', - description: 'moderation.registryRejected', }, [SubmissionReviewStatus.Withdrawn]: { value: SubmissionReviewStatus.Withdrawn, icon: 'fas fa-circle-minus', - description: 'moderation.registryWithdrawal', }, [SubmissionReviewStatus.Public]: { value: SubmissionReviewStatus.Public, icon: 'fas fa-lock', - description: 'moderation.registryAccepted', }, [SubmissionReviewStatus.Embargo]: { value: SubmissionReviewStatus.Embargo, icon: 'fas fa-lock-open', - description: 'moderation.registryAccepted', }, [SubmissionReviewStatus.PendingWithdrawal]: { value: SubmissionReviewStatus.PendingWithdrawal, icon: 'fas fa-circle-minus', - description: 'moderation.registrySubmitted', }, [SubmissionReviewStatus.PendingUpdates]: { value: SubmissionReviewStatus.PendingUpdates, icon: 'fas fa-hourglass', - description: 'moderation.registrySubmitted', }, }; diff --git a/src/app/features/moderation/enums/index.ts b/src/app/features/moderation/enums/index.ts index a3d0d5979..57cd4ff78 100644 --- a/src/app/features/moderation/enums/index.ts +++ b/src/app/features/moderation/enums/index.ts @@ -3,6 +3,7 @@ export * from './collection-moderation-tab.enum'; export * from './moderation-type.enum'; export * from './moderator-permission.enum'; export * from './preprint-moderation-tab.enum'; +export * from './registry-action-state.enum'; export * from './registry-moderation-tab.enum'; export * from './registry-sort.enum'; export * from './settings-section-control.enum'; diff --git a/src/app/features/moderation/enums/registry-action-state.enum.ts b/src/app/features/moderation/enums/registry-action-state.enum.ts new file mode 100644 index 000000000..d79b7f7d2 --- /dev/null +++ b/src/app/features/moderation/enums/registry-action-state.enum.ts @@ -0,0 +1,7 @@ +export enum RegistryActionState { + Pending = 'pending', + Accepted = 'accepted', + Rejected = 'rejected', + Withdrawn = 'withdrawn', + PendingWithdraw = 'pending_withdraw', +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7a4a1d483..d568d95c4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1199,9 +1199,10 @@ "namedDescription": "All comments will be visible to the contributors of the submission and the moderator's osf profile name will be displayed." } }, - "registryAccepted": "Registration submission", + "registryAccepted": "Registration submission accepted", "registrySubmitted": "Registration submitted", - "registryWithdrawal": "Registration withdrawal request accepted", + "registryWithdrawalRequested": "Registration withdrawal requested", + "registryWithdrawalAccepted": "Registration withdrawal request accepted", "registryRejected": "Registration submission rejected", "registryEmbargoedWithEndDate": "Embargoed Registration with an end date of ", "withNoEmbargo": "with no embargo", From a96f69292451beda50d6e914aa4590fe1ee183cd Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 31 Jul 2025 13:35:39 +0300 Subject: [PATCH 3/4] feat(submissions): updated submission for preprints and registries --- .../features/moderation/components/index.ts | 3 + .../my-reviewing-navigation.component.html | 2 +- .../my-reviewing-navigation.component.scss | 4 +- .../preprint-submission-item.component.html | 36 +++++ .../preprint-submission-item.component.scss | 3 + ...preprint-submission-item.component.spec.ts | 22 +++ .../preprint-submission-item.component.ts | 36 +++++ .../preprint-submissions.component.html | 63 ++++++++ .../preprint-submissions.component.scss | 8 + .../preprint-submissions.component.spec.ts | 22 +++ .../preprint-submissions.component.ts | 147 ++++++++++++++++++ ...rint-withdrawal-submissions.component.html | 63 ++++++++ ...rint-withdrawal-submissions.component.scss | 8 + ...t-withdrawal-submissions.component.spec.ts | 22 +++ ...eprint-withdrawal-submissions.component.ts | 144 +++++++++++++++++ .../registry-pending-submissions.component.ts | 21 ++- .../registry-submission-item.component.html | 2 +- .../registry-submission-item.component.scss | 22 --- .../registry-submission-item.component.ts | 7 +- .../registry-submissions.component.scss | 12 -- .../registry-submissions.component.ts | 36 +++-- .../submission-item.component.scss | 15 -- .../features/moderation/constants/index.ts | 2 + .../constants/preprint-action-labels.const.ts | 7 + .../constants/preprint-sort-options.const.ts | 22 +++ .../constants/registry-action-labels.const.ts | 14 +- .../constants/registry-sort-options.const.ts | 8 +- .../moderation/constants/submission.const.ts | 41 ++++- ...on-state.enum.ts => action-status.enum.ts} | 2 +- src/app/features/moderation/enums/index.ts | 3 +- .../enums/preprint-submissions-sort.enum.ts | 6 + .../mappers/preprint-moderation.mapper.ts | 44 +++++- .../mappers/registry-moderation.mapper.ts | 6 +- src/app/features/moderation/models/index.ts | 10 +- .../preprint-submission-json-api.model.ts | 30 ++++ .../models/preprint-submission.model.ts | 18 +++ .../preprint-withdrawal-action.model.ts | 8 + ...nt-withdrawal-submission-json-api.model.ts | 46 ++++++ .../preprint-withdrawal-submission.model.ts | 16 ++ .../models/registry-moderation.model.ts | 4 +- ...del.ts => review-action-json-api.model.ts} | 12 +- ...action.model.ts => review-action.model.ts} | 2 +- .../models/submission-review-option.model.ts | 8 + .../moderation/preprint-moderation.routes.ts | 8 +- .../services/preprint-moderation.service.ts | 55 ++++++- .../services/registry-moderation.service.ts | 6 +- .../preprint-moderation.actions.ts | 24 +++ .../preprint-moderation.model.ts | 37 ++++- .../preprint-moderation.selectors.ts | 65 ++++++++ .../preprint-moderation.state.ts | 108 ++++++++++++- src/assets/i18n/en.json | 7 +- src/assets/styles/_common.scss | 19 +++ src/assets/styles/overrides/button.scss | 5 + 53 files changed, 1223 insertions(+), 118 deletions(-) create mode 100644 src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html create mode 100644 src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.scss create mode 100644 src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts create mode 100644 src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.ts create mode 100644 src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.html create mode 100644 src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.scss create mode 100644 src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts create mode 100644 src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts create mode 100644 src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.html create mode 100644 src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.scss create mode 100644 src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts create mode 100644 src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.ts create mode 100644 src/app/features/moderation/constants/preprint-action-labels.const.ts create mode 100644 src/app/features/moderation/constants/preprint-sort-options.const.ts rename src/app/features/moderation/enums/{registry-action-state.enum.ts => action-status.enum.ts} (80%) create mode 100644 src/app/features/moderation/enums/preprint-submissions-sort.enum.ts create mode 100644 src/app/features/moderation/models/preprint-submission-json-api.model.ts create mode 100644 src/app/features/moderation/models/preprint-submission.model.ts create mode 100644 src/app/features/moderation/models/preprint-withdrawal-action.model.ts create mode 100644 src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts create mode 100644 src/app/features/moderation/models/preprint-withdrawal-submission.model.ts rename src/app/features/moderation/models/{registry-action-json-api.model.ts => review-action-json-api.model.ts} (58%) rename src/app/features/moderation/models/{registry-action.model.ts => review-action.model.ts} (82%) create mode 100644 src/app/features/moderation/models/submission-review-option.model.ts diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts index 3dd125e13..881577e5f 100644 --- a/src/app/features/moderation/components/index.ts +++ b/src/app/features/moderation/components/index.ts @@ -7,6 +7,9 @@ export { ModeratorsTableComponent } from './moderators-table/moderators-table.co export { MyReviewingNavigationComponent } from './my-reviewing-navigation/my-reviewing-navigation.component'; export { NotificationSettingsComponent } from './notification-settings/notification-settings.component'; export { PreprintModerationSettingsComponent } from './preprint-moderation-settings/preprint-moderation-settings.component'; +export { PreprintSubmissionItemComponent } from './preprint-submission-item/preprint-submission-item.component'; +export { PreprintSubmissionsComponent } from './preprint-submissions/preprint-submissions.component'; +export { PreprintWithdrawalSubmissionsComponent } from './preprint-withdrawal-submissions/preprint-withdrawal-submissions.component'; export { RecentActivityListComponent } from './recent-activity-list/recent-activity-list.component'; export { RegistryPendingSubmissionsComponent } from './registry-pending-submissions/registry-pending-submissions.component'; export { RegistrySettingsComponent } from './registry-settings/registry-settings.component'; diff --git a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.html b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.html index 22f41ee90..4ceac0a3e 100644 --- a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.html +++ b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.html @@ -5,7 +5,7 @@

{{ provider().name }}

@let badge = tab.value === tabOption.Submissions ? `${provider().submissionCount}` : undefined; + + +
+ + {{ submission().title }} + + + @for (action of showAll ? submission().actions : submission().actions.slice(0, limitValue); track $index) { +
+ {{ actionLabel[action.toState] | translate }} + {{ action.dateModified | dateAgo }} + {{ 'moderation.submissionReview.by' | translate }} + {{ action.creator.name }} + + @if (action.comment.length) { + - {{ action.comment }} + } +
+ } + + @if (submission().actions.length > 1) { + + } +
+
diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.scss b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.scss new file mode 100644 index 000000000..d0a25f8ad --- /dev/null +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.scss @@ -0,0 +1,3 @@ +.submission-link { + color: var(--dark-blue-1); +} diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts new file mode 100644 index 000000000..dcbdfb1f8 --- /dev/null +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintSubmissionItemComponent } from './preprint-submission-item.component'; + +describe('PreprintSubmissionItemComponent', () => { + let component: PreprintSubmissionItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintSubmissionItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintSubmissionItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.ts b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.ts new file mode 100644 index 000000000..d53e6a994 --- /dev/null +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.ts @@ -0,0 +1,36 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; + +import { PREPRINT_ACTION_LABEL, ReviewStatusIcon } from '@osf/features/moderation/constants'; +import { ActionStatus, SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { IconComponent } from '@osf/shared/components'; +import { DateAgoPipe } from '@osf/shared/pipes'; + +import { PreprintSubmission, PreprintWithdrawalSubmission } from '../../models'; + +@Component({ + selector: 'osf-preprint-submission-item', + imports: [IconComponent, DateAgoPipe, Button, TranslatePipe], + templateUrl: './preprint-submission-item.component.html', + styleUrl: './preprint-submission-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintSubmissionItemComponent { + status = input.required(); + submission = input.required(); + selected = output(); + + readonly reviewStatusIcon = ReviewStatusIcon; + readonly actionLabel = PREPRINT_ACTION_LABEL; + readonly actionState = ActionStatus; + + limitValue = 1; + showAll = false; + + toggleHistory() { + this.showAll = !this.showAll; + } +} diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.html b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.html new file mode 100644 index 000000000..b4ccf677e --- /dev/null +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.html @@ -0,0 +1,63 @@ +
+
+ + + + {{ item.count }} +

{{ item.label | translate | titlecase }}

+
+
+
+ +
+ +
+
+ +@if (isLoading()) { + +} @else { + @if (submissions().length) { +
+ @for (item of submissions(); track $index) { +
+ +
+ } +
+ + @if (totalCount() > pageSize()) { +
+ +
+ } + } @else { +
+

{{ 'moderation.noSubmissions' | translate }}

+
+ } +} diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.scss b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.scss new file mode 100644 index 000000000..f5d291e41 --- /dev/null +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.scss @@ -0,0 +1,8 @@ +.submission-container { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; +} + +.submission-item:not(:last-child) { + border-bottom: 1px solid var(--grey-2); +} diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts new file mode 100644 index 000000000..36cad6756 --- /dev/null +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintSubmissionsComponent } from './preprint-submissions.component'; + +describe('PreprintSubmissionsComponent', () => { + let component: PreprintSubmissionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintSubmissionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintSubmissionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts new file mode 100644 index 000000000..1f0223c57 --- /dev/null +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts @@ -0,0 +1,147 @@ +import { createDispatchMap, createSelectMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { PaginatorState } from 'primeng/paginator'; +import { SelectButton } from 'primeng/selectbutton'; + +import { map, of } from 'rxjs'; + +import { TitleCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Primitive } from '@osf/core/helpers'; +import { PreprintSubmissionItemComponent } from '@osf/features/moderation/components'; +import { PREPRINT_SORT_OPTIONS, SUBMISSION_REVIEW_OPTIONS } from '@osf/features/moderation/constants'; +import { PreprintSubmissionsSort, SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { + CustomPaginatorComponent, + IconComponent, + LoadingSpinnerComponent, + SelectComponent, +} from '@osf/shared/components'; + +import { PreprintSubmission } from '../../models'; +import { GetPreprintSubmissions, PreprintModerationSelectors } from '../../store/preprint-moderation'; + +@Component({ + selector: 'osf-preprint-submissions', + imports: [ + SelectButton, + TranslatePipe, + FormsModule, + SelectComponent, + IconComponent, + TitleCasePipe, + LoadingSpinnerComponent, + PreprintSubmissionItemComponent, + CustomPaginatorComponent, + ], + templateUrl: './preprint-submissions.component.html', + styleUrl: './preprint-submissions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintSubmissionsComponent implements OnInit { + readonly sortOptions = PREPRINT_SORT_OPTIONS; + + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly providerId = toSignal( + this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) + ); + + readonly actions = createDispatchMap({ getPreprintSubmissions: GetPreprintSubmissions }); + + readonly submissions = select(PreprintModerationSelectors.getPreprintSubmissions); + readonly isLoading = select(PreprintModerationSelectors.arePreprintSubmissionsLoading); + readonly counts = createSelectMap({ + [SubmissionReviewStatus.Pending]: PreprintModerationSelectors.getPreprintSubmissionsPendingCount, + [SubmissionReviewStatus.Accepted]: PreprintModerationSelectors.getPreprintSubmissionsAcceptedCount, + [SubmissionReviewStatus.Rejected]: PreprintModerationSelectors.getPreprintSubmissionsRejectedCount, + [SubmissionReviewStatus.Withdrawn]: PreprintModerationSelectors.getPreprintSubmissionsWithdrawnCount, + }); + + readonly currentPage = signal(1); + readonly pageSize = signal(10); + readonly first = signal(0); + readonly selectedSortOption = signal(PreprintSubmissionsSort.Newest); + readonly selectedReviewOption = signal(SUBMISSION_REVIEW_OPTIONS[0].value); + + private readonly countMap: Record number> = { + [SubmissionReviewStatus.Accepted]: () => this.counts[SubmissionReviewStatus.Accepted](), + [SubmissionReviewStatus.Pending]: () => this.counts[SubmissionReviewStatus.Pending](), + [SubmissionReviewStatus.Rejected]: () => this.counts[SubmissionReviewStatus.Rejected](), + [SubmissionReviewStatus.Withdrawn]: () => this.counts[SubmissionReviewStatus.Withdrawn](), + [SubmissionReviewStatus.Public]: () => this.counts[SubmissionReviewStatus.Accepted](), + }; + + submissionReviewOptions = computed(() => + SUBMISSION_REVIEW_OPTIONS.map((option) => ({ ...option, count: this.countMap[option.value]() })) + ); + + totalCount = computed(() => this.countMap[this.selectedReviewOption()]()); + + ngOnInit(): void { + this.getStatusFromQueryParams(); + this.fetchSubmissions(); + } + + changeReviewStatus(value: SubmissionReviewStatus): void { + this.selectedReviewOption.set(value); + this.router.navigate([], { + relativeTo: this.route, + queryParams: { status: value }, + queryParamsHandling: 'merge', + }); + + this.resetPagination(); + this.fetchSubmissions(); + } + + changeSort(value: Primitive): void { + this.selectedSortOption.set(value as PreprintSubmissionsSort); + this.fetchSubmissions(); + } + + onPageChange(event: PaginatorState): void { + this.currentPage.set(event.page ? event.page + 1 : 1); + this.first.set(event.first ?? 0); + this.fetchSubmissions(); + } + + navigateToPreprint(item: PreprintSubmission) { + this.router.navigate(['/preprints/', item.id, 'overview'], { queryParams: { mode: 'moderator' } }); + } + + private getStatusFromQueryParams() { + const queryParams = this.route.snapshot.queryParams; + const statusValues = Object.values(SubmissionReviewStatus); + + const statusParam = queryParams['status']; + + if (statusParam && statusValues.includes(statusParam)) { + this.selectedReviewOption.set(statusParam); + } + } + + private resetPagination(): void { + this.currentPage.set(1); + this.first.set(0); + } + + private fetchSubmissions(): void { + const providerId = this.providerId(); + + if (!providerId) return; + + this.actions.getPreprintSubmissions( + providerId, + this.selectedReviewOption(), + this.currentPage(), + this.selectedSortOption() + ); + } +} diff --git a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.html b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.html new file mode 100644 index 000000000..373ff7684 --- /dev/null +++ b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.html @@ -0,0 +1,63 @@ +
+
+ + + + {{ item.count }} +

{{ item.label | translate | titlecase }}

+
+
+
+ +
+ +
+
+ +@if (isLoading()) { + +} @else { + @if (submissions().length) { +
+ @for (item of submissions(); track item.id) { +
+ +
+ } +
+ + @if (totalCount() > pageSize()) { +
+ +
+ } + } @else { +
+

{{ 'moderation.noSubmissions' | translate }}

+
+ } +} diff --git a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.scss b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.scss new file mode 100644 index 000000000..f5d291e41 --- /dev/null +++ b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.scss @@ -0,0 +1,8 @@ +.submission-container { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; +} + +.submission-item:not(:last-child) { + border-bottom: 1px solid var(--grey-2); +} diff --git a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts new file mode 100644 index 000000000..261e6638b --- /dev/null +++ b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PreprintWithdrawalSubmissionsComponent } from './preprint-withdrawal-submissions.component'; + +describe('PreprintWithdrawalSubmissionsComponent', () => { + let component: PreprintWithdrawalSubmissionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PreprintWithdrawalSubmissionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PreprintWithdrawalSubmissionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.ts b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.ts new file mode 100644 index 000000000..38baf7fb9 --- /dev/null +++ b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.ts @@ -0,0 +1,144 @@ +import { createDispatchMap, createSelectMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { PaginatorState } from 'primeng/paginator'; +import { SelectButton } from 'primeng/selectbutton'; + +import { map, of } from 'rxjs'; + +import { TitleCasePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Primitive } from '@osf/core/helpers'; +import { PREPRINT_SORT_OPTIONS, WITHDRAWAL_SUBMISSION_REVIEW_OPTIONS } from '@osf/features/moderation/constants'; +import { PreprintSubmissionsSort, SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { + CustomPaginatorComponent, + IconComponent, + LoadingSpinnerComponent, + SelectComponent, +} from '@osf/shared/components'; + +import { PreprintWithdrawalSubmission } from '../../models'; +import { GetPreprintWithdrawalSubmissions, PreprintModerationSelectors } from '../../store/preprint-moderation'; +import { PreprintSubmissionItemComponent } from '../preprint-submission-item/preprint-submission-item.component'; + +@Component({ + selector: 'osf-preprint-withdrawal-submissions', + imports: [ + SelectButton, + TranslatePipe, + FormsModule, + SelectComponent, + IconComponent, + TitleCasePipe, + LoadingSpinnerComponent, + PreprintSubmissionItemComponent, + CustomPaginatorComponent, + ], + templateUrl: './preprint-withdrawal-submissions.component.html', + styleUrl: './preprint-withdrawal-submissions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PreprintWithdrawalSubmissionsComponent implements OnInit { + readonly sortOptions = PREPRINT_SORT_OPTIONS; + + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly providerId = toSignal( + this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) + ); + + readonly actions = createDispatchMap({ getPreprintWithdrawalSubmissions: GetPreprintWithdrawalSubmissions }); + + readonly submissions = select(PreprintModerationSelectors.getPreprintWithdrawalSubmissions); + readonly isLoading = select(PreprintModerationSelectors.arePreprintWithdrawalSubmissionsLoading); + readonly counts = createSelectMap({ + [SubmissionReviewStatus.Pending]: PreprintModerationSelectors.getPreprintWithdrawalSubmissionsPendingCount, + [SubmissionReviewStatus.Accepted]: PreprintModerationSelectors.getPreprintWithdrawalSubmissionsAcceptedCount, + [SubmissionReviewStatus.Rejected]: PreprintModerationSelectors.getPreprintWithdrawalSubmissionsRejectedCount, + }); + + private readonly countMap: Record number> = { + [SubmissionReviewStatus.Accepted]: () => this.counts[SubmissionReviewStatus.Accepted](), + [SubmissionReviewStatus.Pending]: () => this.counts[SubmissionReviewStatus.Pending](), + [SubmissionReviewStatus.Rejected]: () => this.counts[SubmissionReviewStatus.Rejected](), + }; + + readonly currentPage = signal(1); + readonly pageSize = signal(10); + readonly first = signal(0); + readonly selectedSortOption = signal(PreprintSubmissionsSort.Newest); + readonly selectedReviewOption = signal(WITHDRAWAL_SUBMISSION_REVIEW_OPTIONS[0].value); + + readonly totalCount = computed(() => this.countMap[this.selectedReviewOption()]() ?? 0); + + submissionReviewOptions = computed(() => + WITHDRAWAL_SUBMISSION_REVIEW_OPTIONS.map((option) => ({ ...option, count: this.countMap[option.value]() })) + ); + + ngOnInit(): void { + this.getStatusFromQueryParams(); + this.fetchSubmissions(); + } + + changeReviewStatus(value: SubmissionReviewStatus): void { + this.selectedReviewOption.set(value); + this.router.navigate([], { + relativeTo: this.route, + queryParams: { status: value }, + queryParamsHandling: 'merge', + }); + + this.resetPagination(); + this.fetchSubmissions(); + } + + changeSort(value: Primitive): void { + this.selectedSortOption.set(value as PreprintSubmissionsSort); + this.fetchSubmissions(); + } + + onPageChange(event: PaginatorState): void { + this.currentPage.set(event.page ? event.page + 1 : 1); + this.first.set(event.first ?? 0); + this.fetchSubmissions(); + } + + navigateToPreprint(item: PreprintWithdrawalSubmission) { + this.router.navigate(['/preprints/', item.preprintId, 'overview'], { queryParams: { mode: 'moderator' } }); + } + + private getStatusFromQueryParams() { + const queryParams = this.route.snapshot.queryParams; + const statusValues = Object.values(SubmissionReviewStatus); + + const statusParam = queryParams['status']; + + if (statusParam && statusValues.includes(statusParam)) { + this.selectedReviewOption.set(statusParam); + } + } + + private resetPagination(): void { + this.currentPage.set(1); + this.first.set(0); + } + + private fetchSubmissions(): void { + const providerId = this.providerId(); + + if (!providerId) return; + + this.actions.getPreprintWithdrawalSubmissions( + providerId, + this.selectedReviewOption(), + this.currentPage(), + this.selectedSortOption() + ); + } +} diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts index 37df7f19b..e76b06997 100644 --- a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts @@ -11,7 +11,7 @@ import { TitleCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Primitive } from '@osf/core/helpers'; import { PENDING_SUBMISSION_REVIEW_OPTIONS, REGISTRY_SORT_OPTIONS } from '@osf/features/moderation/constants'; @@ -51,6 +51,7 @@ export class RegistryPendingSubmissionsComponent implements OnInit { readonly sortOptions = REGISTRY_SORT_OPTIONS; private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); private readonly providerId = toSignal( this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) ); @@ -68,11 +69,18 @@ export class RegistryPendingSubmissionsComponent implements OnInit { readonly selectedReviewOption = signal(this.submissionReviewOptions[0].value); ngOnInit(): void { + this.getStatusFromQueryParams(); this.fetchSubmissions(); } changeReviewStatus(value: SubmissionReviewStatus): void { this.selectedReviewOption.set(value); + this.router.navigate([], { + relativeTo: this.route, + queryParams: { status: value }, + queryParamsHandling: 'merge', + }); + this.resetPagination(); this.fetchSubmissions(); } @@ -88,6 +96,17 @@ export class RegistryPendingSubmissionsComponent implements OnInit { this.fetchSubmissions(); } + private getStatusFromQueryParams() { + const queryParams = this.route.snapshot.queryParams; + const statusValues = Object.values(SubmissionReviewStatus); + + const statusParam = queryParams['status']; + + if (statusParam && statusValues.includes(statusParam)) { + this.selectedReviewOption.set(statusParam); + } + } + private resetPagination(): void { this.currentPage.set(1); this.first.set(0); diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html index 0cf5733cc..c2e70c153 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html @@ -37,7 +37,7 @@ @if (submission().actions.length > 1) { (); submission = input.required(); - selected = output(); readonly reviewStatusIcon = ReviewStatusIcon; readonly registryActionLabel = REGISTRY_ACTION_LABEL; - readonly registryActionState = RegistryActionState; + readonly registryActionState = ActionStatus; limitValue = 1; showAll = false; diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss index d1909e9e4..f5d291e41 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.scss @@ -6,15 +6,3 @@ .submission-item:not(:last-child) { border-bottom: 1px solid var(--grey-2); } - -.pending { - color: var(--yellow-1); -} - -.accepted { - color: var(--green-1); -} - -.rejected { - color: var(--red-1); -} diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts index 6beffeeb0..88a11e68c 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts @@ -8,10 +8,10 @@ import { SelectButton } from 'primeng/selectbutton'; import { map, of } from 'rxjs'; import { TitleCasePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Primitive } from '@osf/core/helpers'; import { @@ -48,6 +48,7 @@ export class RegistrySubmissionsComponent implements OnInit { readonly sortOptions = REGISTRY_SORT_OPTIONS; private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); private readonly providerId = toSignal( this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined) ); @@ -64,18 +65,19 @@ export class RegistrySubmissionsComponent implements OnInit { readonly selectedSortOption = signal(RegistrySort.RegisteredNewest); readonly selectedReviewOption = signal(this.submissionReviewOptions[0].value); - readonly actualStatus = computed(() => - this.selectedReviewOption() === SubmissionReviewStatus.Public - ? SubmissionReviewStatus.Accepted - : this.selectedReviewOption() - ); - ngOnInit(): void { + this.getStatusFromQueryParams(); this.fetchSubmissions(); } changeReviewStatus(value: SubmissionReviewStatus): void { this.selectedReviewOption.set(value); + this.router.navigate([], { + relativeTo: this.route, + queryParams: { status: value }, + queryParamsHandling: 'merge', + }); + this.resetPagination(); this.fetchSubmissions(); } @@ -91,6 +93,17 @@ export class RegistrySubmissionsComponent implements OnInit { this.fetchSubmissions(); } + private getStatusFromQueryParams() { + const queryParams = this.route.snapshot.queryParams; + const statusValues = Object.values(SubmissionReviewStatus); + + const statusParam = queryParams['status']; + + if (statusParam && statusValues.includes(statusParam)) { + this.selectedReviewOption.set(statusParam); + } + } + private resetPagination(): void { this.currentPage.set(1); this.first.set(0); @@ -101,6 +114,11 @@ export class RegistrySubmissionsComponent implements OnInit { if (!providerId) return; - this.actions.getRegistrySubmissions(providerId, this.actualStatus(), this.currentPage(), this.selectedSortOption()); + this.actions.getRegistrySubmissions( + providerId, + this.selectedReviewOption(), + this.currentPage(), + this.selectedSortOption() + ); } } diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.scss b/src/app/features/moderation/components/submission-item/submission-item.component.scss index baf8accf8..e69de29bb 100644 --- a/src/app/features/moderation/components/submission-item/submission-item.component.scss +++ b/src/app/features/moderation/components/submission-item/submission-item.component.scss @@ -1,15 +0,0 @@ -.pending { - color: var(--yellow-1); -} - -.accepted { - color: var(--green-1); -} - -.rejected { - color: var(--red-1); -} - -.withdrawn { - color: var(--dark-blue-1); -} diff --git a/src/app/features/moderation/constants/index.ts b/src/app/features/moderation/constants/index.ts index 44e02f8aa..f10dea5d8 100644 --- a/src/app/features/moderation/constants/index.ts +++ b/src/app/features/moderation/constants/index.ts @@ -1,7 +1,9 @@ export * from './collection-moderation-tabs.const'; export * from './my-preprint-reviewing.const'; +export * from './preprint-action-labels.const'; export * from './preprint-moderation-tabs.const'; export * from './preprint-settings-sections.const'; +export * from './preprint-sort-options.const'; export * from './registry-action-labels.const'; export * from './registry-moderation-tabs.const'; export * from './registry-sort-options.const'; diff --git a/src/app/features/moderation/constants/preprint-action-labels.const.ts b/src/app/features/moderation/constants/preprint-action-labels.const.ts new file mode 100644 index 000000000..9d79fba2c --- /dev/null +++ b/src/app/features/moderation/constants/preprint-action-labels.const.ts @@ -0,0 +1,7 @@ +import { ActionStatus } from '../enums'; + +export const PREPRINT_ACTION_LABEL: Record = { + [ActionStatus.Accepted]: 'moderation.submissionReview.accepted', + [ActionStatus.Pending]: 'moderation.submissionReview.submitted', + [ActionStatus.Rejected]: 'moderation.submissionReview.rejected', +}; diff --git a/src/app/features/moderation/constants/preprint-sort-options.const.ts b/src/app/features/moderation/constants/preprint-sort-options.const.ts new file mode 100644 index 000000000..60dbe6cdb --- /dev/null +++ b/src/app/features/moderation/constants/preprint-sort-options.const.ts @@ -0,0 +1,22 @@ +import { CustomOption } from '@osf/shared/models'; + +import { PreprintSubmissionsSort } from '../enums'; + +export const PREPRINT_SORT_OPTIONS: CustomOption[] = [ + { + value: PreprintSubmissionsSort.TitleAZ, + label: 'moderation.sortOption.titleAZ', + }, + { + value: PreprintSubmissionsSort.TitleZA, + label: 'moderation.sortOption.titleZA', + }, + { + value: PreprintSubmissionsSort.Newest, + label: 'moderation.sortOption.oldest', + }, + { + value: PreprintSubmissionsSort.Oldest, + label: 'moderation.sortOption.newest', + }, +]; diff --git a/src/app/features/moderation/constants/registry-action-labels.const.ts b/src/app/features/moderation/constants/registry-action-labels.const.ts index 3a68dc74d..9edd3d2e2 100644 --- a/src/app/features/moderation/constants/registry-action-labels.const.ts +++ b/src/app/features/moderation/constants/registry-action-labels.const.ts @@ -1,9 +1,9 @@ -import { RegistryActionState } from '../enums'; +import { ActionStatus } from '../enums'; -export const REGISTRY_ACTION_LABEL: Record = { - [RegistryActionState.Accepted]: 'moderation.registryAccepted', - [RegistryActionState.Pending]: 'moderation.registrySubmitted', - [RegistryActionState.PendingWithdraw]: 'moderation.registryWithdrawalRequested', - [RegistryActionState.Withdrawn]: 'moderation.registryWithdrawalAccepted', - [RegistryActionState.Rejected]: 'moderation.registryRejected', +export const REGISTRY_ACTION_LABEL: Record = { + [ActionStatus.Accepted]: 'moderation.registryAccepted', + [ActionStatus.Pending]: 'moderation.registrySubmitted', + [ActionStatus.PendingWithdraw]: 'moderation.registryWithdrawalRequested', + [ActionStatus.Withdrawn]: 'moderation.registryWithdrawalAccepted', + [ActionStatus.Rejected]: 'moderation.registryRejected', }; diff --git a/src/app/features/moderation/constants/registry-sort-options.const.ts b/src/app/features/moderation/constants/registry-sort-options.const.ts index 59479ca8f..7eccf69a8 100644 --- a/src/app/features/moderation/constants/registry-sort-options.const.ts +++ b/src/app/features/moderation/constants/registry-sort-options.const.ts @@ -5,18 +5,18 @@ import { RegistrySort } from '../enums'; export const REGISTRY_SORT_OPTIONS: CustomOption[] = [ { value: RegistrySort.TitleAZ, - label: 'moderation.registrySortOption.titleAZ', + label: 'moderation.sortOption.titleAZ', }, { value: RegistrySort.TitleZA, - label: 'moderation.registrySortOption.titleZA', + label: 'moderation.sortOption.titleZA', }, { value: RegistrySort.RegisteredNewest, - label: 'moderation.registrySortOption.registeredOldest', + label: 'moderation.sortOption.oldest', }, { value: RegistrySort.RegisteredOldest, - label: 'moderation.registrySortOption.registeredNewest', + label: 'moderation.sortOption.newest', }, ]; diff --git a/src/app/features/moderation/constants/submission.const.ts b/src/app/features/moderation/constants/submission.const.ts index eceefebfa..393e8ec2a 100644 --- a/src/app/features/moderation/constants/submission.const.ts +++ b/src/app/features/moderation/constants/submission.const.ts @@ -1,37 +1,63 @@ import { SubmissionReviewStatus } from '../enums'; +import { SubmissionReviewOption } from '../models'; -export const SUBMISSION_REVIEW_OPTIONS = [ +export const SUBMISSION_REVIEW_OPTIONS: SubmissionReviewOption[] = [ { value: SubmissionReviewStatus.Pending, icon: 'fas fa-hourglass', label: 'moderation.submissionReviewStatus.pending', + count: 0, }, { value: SubmissionReviewStatus.Accepted, icon: 'fas fa-circle-check', label: 'moderation.submissionReviewStatus.accepted', + count: 0, }, { value: SubmissionReviewStatus.Rejected, icon: 'fas fa-circle-xmark', label: 'moderation.submissionReviewStatus.rejected', + count: 0, }, { value: SubmissionReviewStatus.Withdrawn, icon: 'fas fa-circle-minus', label: 'moderation.submissionReviewStatus.withdrawn', + count: 0, }, ]; -export const SUBMITTED_SUBMISSION_REVIEW_OPTIONS = [ +export const WITHDRAWAL_SUBMISSION_REVIEW_OPTIONS: SubmissionReviewOption[] = [ { - value: SubmissionReviewStatus.Public, + value: SubmissionReviewStatus.Pending, + icon: 'fas fa-hourglass', + label: 'moderation.submissionReviewStatus.pending', + count: 0, + }, + { + value: SubmissionReviewStatus.Accepted, + icon: 'fas fa-circle-check', + label: 'moderation.submissionReviewStatus.accepted', + count: 0, + }, + { + value: SubmissionReviewStatus.Rejected, + icon: 'fas fa-circle-minus', + label: 'moderation.submissionReviewStatus.rejected', + count: 0, + }, +]; + +export const SUBMITTED_SUBMISSION_REVIEW_OPTIONS: SubmissionReviewOption[] = [ + { + value: SubmissionReviewStatus.Accepted, icon: 'fas fa-circle-check', label: 'moderation.submissionReviewStatus.public', }, { value: SubmissionReviewStatus.Embargo, - icon: 'fas fa-lock-open', + icon: 'fas fa-lock', label: 'moderation.submissionReviewStatus.embargo', }, { @@ -46,12 +72,11 @@ export const SUBMITTED_SUBMISSION_REVIEW_OPTIONS = [ }, ]; -export const PENDING_SUBMISSION_REVIEW_OPTIONS = [ +export const PENDING_SUBMISSION_REVIEW_OPTIONS: SubmissionReviewOption[] = [ { value: SubmissionReviewStatus.Pending, icon: 'fas fa-hourglass', label: 'moderation.submissionReviewStatus.pending', - description: 'moderation.registrySubmitted', }, { value: SubmissionReviewStatus.PendingUpdates, @@ -84,11 +109,11 @@ export const ReviewStatusIcon: Record ({ + id: x.id, + title: x.attributes.title, + public: x.attributes.public, + reviewsState: x.attributes.reviews_state, + actions: [], + })), + totalCount: response.meta.total, + pendingCount: response.meta.reviews_state_counts.pending, + acceptedCount: response.meta.reviews_state_counts.accepted, + rejectedCount: response.meta.reviews_state_counts.rejected, + withdrawnCount: response.meta.reviews_state_counts.withdrawn, + }; + } + + static fromWithdrawalSubmissionResponse( + response: PreprintSubmissionWithdrawalResponseJsonApi + ): PreprintWithdrawalPaginatedData { + return { + data: response.data.map((x) => ({ + id: x.id, + title: x.embeds.target.data.attributes.title, + preprintId: x.embeds.target.data.id, + actions: [], + })), + totalCount: response.meta.total, + pendingCount: response.meta.requests_state_counts.pending, + acceptedCount: response.meta.requests_state_counts.accepted, + rejectedCount: response.meta.requests_state_counts.rejected, + }; + } } diff --git a/src/app/features/moderation/mappers/registry-moderation.mapper.ts b/src/app/features/moderation/mappers/registry-moderation.mapper.ts index 6947c989f..704773cdf 100644 --- a/src/app/features/moderation/mappers/registry-moderation.mapper.ts +++ b/src/app/features/moderation/mappers/registry-moderation.mapper.ts @@ -1,11 +1,11 @@ import { PaginatedData } from '@osf/shared/models'; import { - RegistryAction, - RegistryActionsDataJsonApi, RegistryDataJsonApi, RegistryModeration, RegistryResponseJsonApi, + ReviewAction, + ReviewActionsDataJsonApi, } from '../models'; export class RegistryModerationMapper { @@ -28,7 +28,7 @@ export class RegistryModerationMapper { }; } - static fromActionResponse(response: RegistryActionsDataJsonApi): RegistryAction { + static fromActionResponse(response: ReviewActionsDataJsonApi): ReviewAction { return { id: response.id, fromState: response.attributes.from_state, diff --git a/src/app/features/moderation/models/index.ts b/src/app/features/moderation/models/index.ts index ab65f511d..079a4dc94 100644 --- a/src/app/features/moderation/models/index.ts +++ b/src/app/features/moderation/models/index.ts @@ -7,8 +7,14 @@ export * from './preprint-provider-moderation-info.model'; export * from './preprint-related-count-json-api.model'; export * from './preprint-review-action.model'; export * from './preprint-review-action-json-api.model'; -export * from './registry-action.model'; -export * from './registry-action-json-api.model'; +export * from './preprint-submission.model'; +export * from './preprint-submission-json-api.model'; +export * from './preprint-withdrawal-action.model'; +export * from './preprint-withdrawal-submission.model'; +export * from './preprint-withdrawal-submission-json-api.model'; export * from './registry-json-api.model'; export * from './registry-moderation.model'; +export * from './review-action.model'; +export * from './review-action-json-api.model'; export * from './submission.model'; +export * from './submission-review-option.model'; diff --git a/src/app/features/moderation/models/preprint-submission-json-api.model.ts b/src/app/features/moderation/models/preprint-submission-json-api.model.ts new file mode 100644 index 000000000..6952496cd --- /dev/null +++ b/src/app/features/moderation/models/preprint-submission-json-api.model.ts @@ -0,0 +1,30 @@ +import { JsonApiResponseWithMeta, MetaJsonApi } from '@osf/core/models'; + +export type PreprintSubmissionResponseJsonApi = JsonApiResponseWithMeta< + PreprintSubmissionDataJsonApi[], + PreprintSubmissionMetaJsonApi, + null +>; + +export interface PreprintSubmissionDataJsonApi { + id: string; + attributes: PreprintSubmissionAttributesJsonApi; +} + +interface PreprintSubmissionMetaJsonApi extends MetaJsonApi { + reviews_state_counts: { + pending: number; + accepted: number; + rejected: number; + withdrawn: number; + }; +} + +interface PreprintSubmissionAttributesJsonApi { + id: string; + title: string; + reviews_state: string; + public: boolean; + embargoed: boolean; + embargo_end_date: string; +} diff --git a/src/app/features/moderation/models/preprint-submission.model.ts b/src/app/features/moderation/models/preprint-submission.model.ts new file mode 100644 index 000000000..1a829ee38 --- /dev/null +++ b/src/app/features/moderation/models/preprint-submission.model.ts @@ -0,0 +1,18 @@ +import { PaginatedData } from '@osf/shared/models'; + +import { ReviewAction } from './review-action.model'; + +export interface PreprintSubmissionPaginatedData extends PaginatedData { + pendingCount: number; + acceptedCount: number; + rejectedCount: number; + withdrawnCount: number; +} + +export interface PreprintSubmission { + id: string; + title: string; + reviewsState: string; + public: boolean; + actions: ReviewAction[]; +} diff --git a/src/app/features/moderation/models/preprint-withdrawal-action.model.ts b/src/app/features/moderation/models/preprint-withdrawal-action.model.ts new file mode 100644 index 000000000..f8814177e --- /dev/null +++ b/src/app/features/moderation/models/preprint-withdrawal-action.model.ts @@ -0,0 +1,8 @@ +import { IdName } from '@osf/shared/models'; + +export interface PreprintWithdrawalAction { + id: string; + dateModified: string; + creator: IdName; + comment: string; +} diff --git a/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts b/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts new file mode 100644 index 000000000..78901c33a --- /dev/null +++ b/src/app/features/moderation/models/preprint-withdrawal-submission-json-api.model.ts @@ -0,0 +1,46 @@ +import { JsonApiResponseWithMeta, MetaJsonApi } from '@osf/core/models'; + +export type PreprintSubmissionWithdrawalResponseJsonApi = JsonApiResponseWithMeta< + PreprintWithdrawalSubmissionDataJsonApi[], + PreprintWithdrawalSubmissionMetaJsonApi, + null +>; + +export interface PreprintWithdrawalSubmissionDataJsonApi { + id: string; + attributes: PreprintWithdrawalSubmissionAttributesJsonApi; + embeds: PreprintWithdrawalSubmissionEmbedsJsonApi; +} + +interface PreprintWithdrawalSubmissionMetaJsonApi extends MetaJsonApi { + requests_state_counts: { + pending: number; + accepted: number; + rejected: number; + }; +} + +interface PreprintWithdrawalSubmissionAttributesJsonApi { + comment: string; + machine_state: string; + date_last_transitioned: string; +} + +interface PreprintWithdrawalSubmissionEmbedsJsonApi { + target: { + data: { + id: string; + attributes: { + title: string; + }; + }; + }; + creator: { + data: { + id: string; + attributes: { + full_name: string; + }; + }; + }; +} diff --git a/src/app/features/moderation/models/preprint-withdrawal-submission.model.ts b/src/app/features/moderation/models/preprint-withdrawal-submission.model.ts new file mode 100644 index 000000000..a7490e009 --- /dev/null +++ b/src/app/features/moderation/models/preprint-withdrawal-submission.model.ts @@ -0,0 +1,16 @@ +import { PaginatedData } from '@osf/shared/models'; + +import { ReviewAction } from './review-action.model'; + +export interface PreprintWithdrawalPaginatedData extends PaginatedData { + pendingCount: number; + acceptedCount: number; + rejectedCount: number; +} + +export interface PreprintWithdrawalSubmission { + id: string; + title: string; + preprintId: string; + actions: ReviewAction[]; +} diff --git a/src/app/features/moderation/models/registry-moderation.model.ts b/src/app/features/moderation/models/registry-moderation.model.ts index 3f56b3891..963bee893 100644 --- a/src/app/features/moderation/models/registry-moderation.model.ts +++ b/src/app/features/moderation/models/registry-moderation.model.ts @@ -1,4 +1,4 @@ -import { RegistryAction } from './registry-action.model'; +import { ReviewAction } from './review-action.model'; export interface RegistryModeration { id: string; @@ -7,5 +7,5 @@ export interface RegistryModeration { public: boolean; embargoed: boolean; embargoEndDate?: string; - actions: RegistryAction[]; + actions: ReviewAction[]; } diff --git a/src/app/features/moderation/models/registry-action-json-api.model.ts b/src/app/features/moderation/models/review-action-json-api.model.ts similarity index 58% rename from src/app/features/moderation/models/registry-action-json-api.model.ts rename to src/app/features/moderation/models/review-action-json-api.model.ts index a62bfa493..253cdff57 100644 --- a/src/app/features/moderation/models/registry-action-json-api.model.ts +++ b/src/app/features/moderation/models/review-action-json-api.model.ts @@ -1,14 +1,14 @@ import { JsonApiResponse } from '@osf/core/models'; -export type RegistryActionsResponseJsonApi = JsonApiResponse; +export type ReviewActionsResponseJsonApi = JsonApiResponse; -export interface RegistryActionsDataJsonApi { +export interface ReviewActionsDataJsonApi { id: string; - attributes: RegistryActionAttributesJsonApi; - embeds: RegistryActionEmbedsJsonApi; + attributes: ReviewActionAttributesJsonApi; + embeds: ReviewActionEmbedsJsonApi; } -interface RegistryActionAttributesJsonApi { +interface ReviewActionAttributesJsonApi { auto: boolean; comment: string; date_created: string; @@ -19,7 +19,7 @@ interface RegistryActionAttributesJsonApi { visible: true; } -interface RegistryActionEmbedsJsonApi { +interface ReviewActionEmbedsJsonApi { creator: { data: UserModelJsonApi; }; diff --git a/src/app/features/moderation/models/registry-action.model.ts b/src/app/features/moderation/models/review-action.model.ts similarity index 82% rename from src/app/features/moderation/models/registry-action.model.ts rename to src/app/features/moderation/models/review-action.model.ts index 476977c58..7d3242588 100644 --- a/src/app/features/moderation/models/registry-action.model.ts +++ b/src/app/features/moderation/models/review-action.model.ts @@ -1,6 +1,6 @@ import { IdName } from '@osf/shared/models'; -export interface RegistryAction { +export interface ReviewAction { id: string; fromState: string; toState: string; diff --git a/src/app/features/moderation/models/submission-review-option.model.ts b/src/app/features/moderation/models/submission-review-option.model.ts new file mode 100644 index 000000000..278dc53ef --- /dev/null +++ b/src/app/features/moderation/models/submission-review-option.model.ts @@ -0,0 +1,8 @@ +import { SubmissionReviewStatus } from '../enums'; + +export interface SubmissionReviewOption { + value: SubmissionReviewStatus; + icon: string; + label: string; + count?: number; +} diff --git a/src/app/features/moderation/preprint-moderation.routes.ts b/src/app/features/moderation/preprint-moderation.routes.ts index ef2a0053f..b7efe141d 100644 --- a/src/app/features/moderation/preprint-moderation.routes.ts +++ b/src/app/features/moderation/preprint-moderation.routes.ts @@ -25,16 +25,16 @@ export const preprintModerationRoutes: Routes = [ { path: 'submissions', loadComponent: () => - import('./components/registry-submissions/registry-submissions.component').then( - (m) => m.RegistrySubmissionsComponent + import('./components/preprint-submissions/preprint-submissions.component').then( + (m) => m.PreprintSubmissionsComponent ), data: { tab: PreprintModerationTab.Submissions }, }, { path: 'withdrawals', loadComponent: () => - import('./components/collection-moderation-submissions/collection-moderation-submissions.component').then( - (m) => m.CollectionModerationSubmissionsComponent + import('./components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component').then( + (m) => m.PreprintWithdrawalSubmissionsComponent ), data: { tab: PreprintModerationTab.WithdrawalRequests }, }, diff --git a/src/app/features/moderation/services/preprint-moderation.service.ts b/src/app/features/moderation/services/preprint-moderation.service.ts index e5d905dfc..99c56f658 100644 --- a/src/app/features/moderation/services/preprint-moderation.service.ts +++ b/src/app/features/moderation/services/preprint-moderation.service.ts @@ -6,13 +6,20 @@ import { JsonApiResponse, JsonApiResponseWithPaging } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; import { PaginatedData } from '@osf/shared/models'; -import { PreprintModerationMapper } from '../mappers'; +import { PreprintSubmissionsSort } from '../enums'; +import { PreprintModerationMapper, RegistryModerationMapper } from '../mappers'; import { PreprintProviderModerationInfo, PreprintRelatedCountJsonApi, PreprintReviewActionModel, + PreprintSubmissionResponseJsonApi, + PreprintSubmissionWithdrawalResponseJsonApi, + PreprintWithdrawalPaginatedData, + ReviewAction, ReviewActionJsonApi, + ReviewActionsResponseJsonApi, } from '../models'; +import { PreprintSubmissionPaginatedData } from '../models/preprint-submission.model'; import { environment } from 'src/environments/environment'; @@ -45,4 +52,50 @@ export class PreprintModerationService { .get>(baseUrl) .pipe(map((response) => PreprintModerationMapper.fromResponseWithPagination(response))); } + + getPreprintSubmissions( + provider: string, + status: string, + page = 1, + sort = PreprintSubmissionsSort.Newest + ): Observable { + const filters = `filter[reviews_state]=${status}`; + + const baseUrl = `${environment.apiUrl}/providers/preprints/${provider}/preprints/?page=${page}&meta[reviews_state_counts]=true&${filters}&sort=${sort}`; + + return this.jsonApiService + .get(baseUrl) + .pipe(map((response) => PreprintModerationMapper.fromSubmissionResponse(response))); + } + + getPreprintWithdrawalSubmissions( + provider: string, + status: string, + page = 1, + sort = PreprintSubmissionsSort.Newest + ): Observable { + const params = `?embed=target&embed=creator&filter[machine_state]=${status}&meta[requests_state_counts]=true&page=${page}&sort=${sort}`; + + const baseUrl = `${environment.apiUrl}/providers/preprints/${provider}/withdraw_requests/${params}`; + + return this.jsonApiService + .get(baseUrl) + .pipe(map((response) => PreprintModerationMapper.fromWithdrawalSubmissionResponse(response))); + } + + getPreprintSubmissionReviewAction(id: string): Observable { + const baseUrl = `${environment.apiUrl}/preprints/${id}/review_actions/`; + + return this.jsonApiService + .get(baseUrl) + .pipe(map((response) => response.data.map((x) => RegistryModerationMapper.fromActionResponse(x)))); + } + + getPreprintWithdrawalSubmissionReviewAction(id: string): Observable { + const baseUrl = `${environment.apiUrl}/requests/${id}/actions/`; + + return this.jsonApiService + .get(baseUrl) + .pipe(map((response) => response.data.map((x) => RegistryModerationMapper.fromActionResponse(x)))); + } } diff --git a/src/app/features/moderation/services/registry-moderation.service.ts b/src/app/features/moderation/services/registry-moderation.service.ts index b2a9543ee..b59a9752d 100644 --- a/src/app/features/moderation/services/registry-moderation.service.ts +++ b/src/app/features/moderation/services/registry-moderation.service.ts @@ -7,7 +7,7 @@ import { PaginatedData } from '@osf/shared/models'; import { RegistrySort, SubmissionReviewStatus } from '../enums'; import { RegistryModerationMapper } from '../mappers'; -import { RegistryAction, RegistryActionsResponseJsonApi, RegistryModeration, RegistryResponseJsonApi } from '../models'; +import { RegistryModeration, RegistryResponseJsonApi, ReviewAction, ReviewActionsResponseJsonApi } from '../models'; import { environment } from 'src/environments/environment'; @@ -35,11 +35,11 @@ export class RegistryModerationService { .pipe(map((response) => RegistryModerationMapper.fromResponseWithPagination(response))); } - getRegistrySubmissionHistory(id: string): Observable { + getRegistrySubmissionHistory(id: string): Observable { const baseUrl = `${environment.apiUrl}/registrations/${id}/actions/`; return this.jsonApiService - .get(baseUrl) + .get(baseUrl) .pipe(map((response) => response.data.map((x) => RegistryModerationMapper.fromActionResponse(x)))); } } diff --git a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.actions.ts b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.actions.ts index 8a7bc49af..86a611229 100644 --- a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.actions.ts +++ b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.actions.ts @@ -1,3 +1,5 @@ +import { PreprintSubmissionsSort } from '../../enums'; + const ACTION_SCOPE = '[Preprint Moderation]'; export class GetPreprintProviders { @@ -15,3 +17,25 @@ export class GetPreprintProvider { constructor(public providerId: string) {} } + +export class GetPreprintSubmissions { + static readonly type = `${ACTION_SCOPE} Get Preprint Submissions`; + + constructor( + public provider: string, + public status: string, + public page?: number, + public sort?: PreprintSubmissionsSort + ) {} +} + +export class GetPreprintWithdrawalSubmissions { + static readonly type = `${ACTION_SCOPE} Get Preprint Withdrawal Submissions`; + + constructor( + public provider: string, + public status: string, + public page?: number, + public sort?: PreprintSubmissionsSort + ) {} +} diff --git a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.model.ts b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.model.ts index 6ea91f8f8..c65ab3d11 100644 --- a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.model.ts +++ b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.model.ts @@ -1,10 +1,26 @@ import { AsyncStateModel, AsyncStateWithTotalCount } from '@osf/shared/models'; -import { PreprintProviderModerationInfo, PreprintReviewActionModel } from '../../models'; +import { PreprintProviderModerationInfo, PreprintReviewActionModel, PreprintWithdrawalSubmission } from '../../models'; +import { PreprintSubmission } from '../../models/preprint-submission.model'; export interface PreprintModerationStateModel { preprintProviders: AsyncStateModel; reviewActions: AsyncStateWithTotalCount; + submissions: SubmissionsWithCount; + withdrawalSubmissions: WithdrawalSubmissionsWithCount; +} + +interface SubmissionsWithCount extends AsyncStateWithTotalCount { + pendingCount: number; + acceptedCount: number; + rejectedCount: number; + withdrawnCount: number; +} + +interface WithdrawalSubmissionsWithCount extends AsyncStateWithTotalCount { + pendingCount: number; + acceptedCount: number; + rejectedCount: number; } export const PREPRINT_MODERATION_STATE_DEFAULTS: PreprintModerationStateModel = { @@ -19,4 +35,23 @@ export const PREPRINT_MODERATION_STATE_DEFAULTS: PreprintModerationStateModel = error: null, totalCount: 0, }, + submissions: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + pendingCount: 0, + acceptedCount: 0, + rejectedCount: 0, + withdrawnCount: 0, + }, + withdrawalSubmissions: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + pendingCount: 0, + acceptedCount: 0, + rejectedCount: 0, + }, }; diff --git a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.selectors.ts b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.selectors.ts index c59c38559..8d5d276bc 100644 --- a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.selectors.ts +++ b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.selectors.ts @@ -33,4 +33,69 @@ export class PreprintModerationSelectors { static getPreprintReviewsTotalCount(state: PreprintModerationStateModel) { return state.reviewActions.totalCount; } + + @Selector([PreprintModerationState]) + static getPreprintSubmissions(state: PreprintModerationStateModel) { + return state.submissions.data; + } + + @Selector([PreprintModerationState]) + static arePreprintSubmissionsLoading(state: PreprintModerationStateModel) { + return state.submissions.isLoading; + } + + @Selector([PreprintModerationState]) + static getPreprintSubmissionsTotalCount(state: PreprintModerationStateModel) { + return state.submissions.totalCount; + } + + @Selector([PreprintModerationState]) + static getPreprintSubmissionsPendingCount(state: PreprintModerationStateModel) { + return state.submissions.pendingCount; + } + + @Selector([PreprintModerationState]) + static getPreprintSubmissionsAcceptedCount(state: PreprintModerationStateModel) { + return state.submissions.acceptedCount; + } + + @Selector([PreprintModerationState]) + static getPreprintSubmissionsRejectedCount(state: PreprintModerationStateModel) { + return state.submissions.rejectedCount; + } + + @Selector([PreprintModerationState]) + static getPreprintSubmissionsWithdrawnCount(state: PreprintModerationStateModel) { + return state.submissions.withdrawnCount; + } + + @Selector([PreprintModerationState]) + static getPreprintWithdrawalSubmissions(state: PreprintModerationStateModel) { + return state.withdrawalSubmissions.data; + } + + @Selector([PreprintModerationState]) + static arePreprintWithdrawalSubmissionsLoading(state: PreprintModerationStateModel) { + return state.withdrawalSubmissions.isLoading; + } + + @Selector([PreprintModerationState]) + static getPreprintWithdrawalSubmissionsTotalCount(state: PreprintModerationStateModel) { + return state.withdrawalSubmissions.totalCount; + } + + @Selector([PreprintModerationState]) + static getPreprintWithdrawalSubmissionsPendingCount(state: PreprintModerationStateModel) { + return state.withdrawalSubmissions.pendingCount; + } + + @Selector([PreprintModerationState]) + static getPreprintWithdrawalSubmissionsAcceptedCount(state: PreprintModerationStateModel) { + return state.withdrawalSubmissions.acceptedCount; + } + + @Selector([PreprintModerationState]) + static getPreprintWithdrawalSubmissionsRejectedCount(state: PreprintModerationStateModel) { + return state.withdrawalSubmissions.rejectedCount; + } } diff --git a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.state.ts b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.state.ts index 950c79d02..35cb6b0d3 100644 --- a/src/app/features/moderation/store/preprint-moderation/preprint-moderation.state.ts +++ b/src/app/features/moderation/store/preprint-moderation/preprint-moderation.state.ts @@ -1,15 +1,22 @@ import { Action, State, StateContext } from '@ngxs/store'; import { insertItem, patch, updateItem } from '@ngxs/store/operators'; -import { catchError, forkJoin, map, switchMap, tap } from 'rxjs'; +import { catchError, forkJoin, map, of, switchMap, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/core/handlers'; +import { PreprintSubmissionPaginatedData, PreprintWithdrawalPaginatedData } from '../../models'; import { PreprintModerationService } from '../../services'; -import { GetPreprintProvider, GetPreprintProviders, GetPreprintReviewActions } from './preprint-moderation.actions'; +import { + GetPreprintProvider, + GetPreprintProviders, + GetPreprintReviewActions, + GetPreprintSubmissions, + GetPreprintWithdrawalSubmissions, +} from './preprint-moderation.actions'; import { PREPRINT_MODERATION_STATE_DEFAULTS, PreprintModerationStateModel } from './preprint-moderation.model'; @State({ @@ -91,4 +98,101 @@ export class PreprintModerationState { catchError((error) => handleSectionError(ctx, 'preprintProviders', error)) ); } + + @Action(GetPreprintSubmissions) + getPreprintSubmissions( + ctx: StateContext, + { provider, status, page, sort }: GetPreprintSubmissions + ) { + ctx.setState(patch({ submissions: patch({ isLoading: true }) })); + + return this.preprintModerationService.getPreprintSubmissions(provider, status, page, sort).pipe( + switchMap((res) => { + if (!res.data.length) { + return of({ + ...res, + data: [], + }); + } + + const actionRequests = res.data.map((item) => + this.preprintModerationService.getPreprintSubmissionReviewAction(item.id) + ); + + return forkJoin(actionRequests).pipe( + map( + (actions) => + ({ + ...res, + data: res.data.map((item, i) => ({ ...item, actions: actions[i] })), + }) as PreprintSubmissionPaginatedData + ) + ); + }), + tap((res) => { + ctx.setState( + patch({ + submissions: patch({ + data: res.data, + isLoading: false, + totalCount: res.totalCount, + acceptedCount: res.acceptedCount, + rejectedCount: res.rejectedCount, + pendingCount: res.pendingCount, + withdrawnCount: res.withdrawnCount, + }), + }) + ); + }), + catchError((error) => handleSectionError(ctx, 'submissions', error)) + ); + } + + @Action(GetPreprintWithdrawalSubmissions) + getPreprintWithdrawalSubmissions( + ctx: StateContext, + { provider, status, page, sort }: GetPreprintWithdrawalSubmissions + ) { + ctx.setState(patch({ withdrawalSubmissions: patch({ isLoading: true }) })); + + return this.preprintModerationService.getPreprintWithdrawalSubmissions(provider, status, page, sort).pipe( + switchMap((res) => { + if (!res.data.length) { + return of({ + ...res, + data: [], + }); + } + + const actionRequests = res.data.map((item) => + this.preprintModerationService.getPreprintWithdrawalSubmissionReviewAction(item.id) + ); + + return forkJoin(actionRequests).pipe( + map( + (actions) => + ({ + ...res, + data: res.data.map((item, i) => ({ ...item, actions: actions[i] })), + }) as PreprintWithdrawalPaginatedData + ) + ); + }), + tap((res) => { + ctx.setState( + patch({ + withdrawalSubmissions: patch({ + data: res.data, + isLoading: false, + totalCount: res.totalCount, + acceptedCount: res.acceptedCount, + rejectedCount: res.rejectedCount, + pendingCount: res.pendingCount, + }), + }) + ); + }), + catchError((error) => handleSectionError(ctx, 'withdrawalSubmissions', error)) + ); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index d568d95c4..0fa46089e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1164,6 +1164,7 @@ "submitted": "Submitted", "accepted": "Accepted", "rejected": "Rejected", + "requested": "Requested", "by": "by" }, "preprintReviewStatus": { @@ -1207,11 +1208,11 @@ "registryEmbargoedWithEndDate": "Embargoed Registration with an end date of ", "withNoEmbargo": "with no embargo", "showHistory": "Show history", - "registrySortOption": { + "sortOption": { "titleAZ": "Title: A-Z", "titleZA": "Title: Z-A", - "registeredOldest": "Date: oldest to newest", - "registeredNewest": "Date: newest to oldest" + "oldest": "Date: oldest to newest", + "newest": "Date: newest to oldest" } }, "settings": { diff --git a/src/assets/styles/_common.scss b/src/assets/styles/_common.scss index b370318a7..8fca09b61 100644 --- a/src/assets/styles/_common.scss +++ b/src/assets/styles/_common.scss @@ -113,3 +113,22 @@ font-weight: 700; color: var(--red-1); } + +// ------------------------- Moderation status ------------------------- + +.pending, +.pending-updates { + color: var(--yellow-1); +} + +.accepted { + color: var(--green-1); +} + +.rejected { + color: var(--red-1); +} + +.withdrawn { + color: var(--dark-blue-1); +} diff --git a/src/assets/styles/overrides/button.scss b/src/assets/styles/overrides/button.scss index a5f230e55..4c2c50442 100644 --- a/src/assets/styles/overrides/button.scss +++ b/src/assets/styles/overrides/button.scss @@ -64,3 +64,8 @@ width: 100%; } } + +.no-link-btn-padding { + --p-button-padding-y: 0; + --p-button-padding-x: 0; +} From ba9464843d324af6889fa339af556fb912db17a9 Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 1 Aug 2025 13:11:33 +0300 Subject: [PATCH 4/4] fix(submissions): updated submissions --- ...tion-moderation-submissions.component.html | 2 +- ...tion-moderation-submissions.component.scss | 15 --- ...ection-moderation-submissions.component.ts | 2 +- .../collection-submission-item.component.html | 13 +- .../collection-submission-item.component.ts | 4 +- .../preprint-submission-item.component.html | 9 +- .../registry-submission-item.component.html | 10 +- .../registry-submission-item.component.scss | 3 - .../moderation/components/test-data.ts | 113 ------------------ .../make-decision-dialog.component.html | 2 +- .../make-decision-dialog.component.ts | 4 +- src/app/shared/pipes/time-ago.pipe.ts | 41 ------- 12 files changed, 16 insertions(+), 202 deletions(-) delete mode 100644 src/app/features/moderation/components/test-data.ts delete mode 100644 src/app/shared/pipes/time-ago.pipe.ts diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html index f3f43114b..d82ef794d 100644 --- a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html @@ -31,7 +31,7 @@ @if (collectionSubmissions().length > pageSize) { -
+
- - +

@@ -38,7 +31,7 @@ {{ 'moderation.submissionReview.withdrawn' | translate }} } } - {{ action.dateCreated | timeAgo }} + {{ action.dateCreated | dateAgo }} {{ 'moderation.submissionReview.by' | translate }} {{ action.createdBy }}

diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts index 8ccb6af20..7f392f299 100644 --- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts @@ -11,15 +11,15 @@ import { ActivatedRoute, Router } from '@angular/router'; import { collectionFilterNames } from '@osf/features/collections/constants'; import { SubmissionReviewStatus } from '@osf/features/moderation/enums'; import { IconComponent } from '@osf/shared/components'; +import { DateAgoPipe } from '@osf/shared/pipes'; import { CollectionSubmission } from '@shared/models'; -import { TimeAgoPipe } from '@shared/pipes/time-ago.pipe'; import { CollectionsSelectors } from '@shared/stores'; import { ReviewStatusIcon } from '../../constants'; @Component({ selector: 'osf-submission-item', - imports: [TranslatePipe, IconComponent, TimeAgoPipe, Button], + imports: [TranslatePipe, IconComponent, DateAgoPipe, Button], templateUrl: './collection-submission-item.component.html', styleUrl: './collection-submission-item.component.scss', providers: [DialogService], diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html index 1814b0667..49a4e7a78 100644 --- a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.html @@ -2,14 +2,7 @@
- - {{ submission().title }} - + @for (action of showAll ? submission().actions : submission().actions.slice(0, limitValue); track $index) {
diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html index 20b5dd784..d94bab9db 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html @@ -2,13 +2,13 @@
- - {{ submission().title }} - + /> @if (submission().public && !submission().embargoEndDate) {

{{ 'registry.overview.statuses.accepted.text' | translate }}

diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss index d0a25f8ad..e69de29bb 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.scss @@ -1,3 +0,0 @@ -.submission-link { - color: var(--dark-blue-1); -} diff --git a/src/app/features/moderation/components/test-data.ts b/src/app/features/moderation/components/test-data.ts deleted file mode 100644 index f41444220..000000000 --- a/src/app/features/moderation/components/test-data.ts +++ /dev/null @@ -1,113 +0,0 @@ -export const pendingReviews = [ - { - id: 'p1', - name: 'Pending Review 1', - reviewStatus: 'pending', - dateSubmitted: '10 mins ago', - submittedBy: 'Alice Green', - }, - { - id: 'p2', - name: 'Pending Review 2', - reviewStatus: 'pending', - dateSubmitted: '33 mins ago', - submittedBy: 'Ben Carter', - }, - { - id: 'p3', - name: 'Pending Review 3', - reviewStatus: 'pending', - dateSubmitted: '2 hours ago', - submittedBy: 'Cara Kim', - }, - { - id: 'p4', - name: 'Pending Review 4', - reviewStatus: 'pending', - dateSubmitted: '1 day ago', - submittedBy: 'David Lin', - }, - { - id: 'p5', - name: 'Pending Review 5', - reviewStatus: 'pending', - dateSubmitted: '1 week ago', - submittedBy: 'Ella Nguyen', - }, -]; - -export const pubicReviews = [ - { - id: 'p1', - name: 'Public Review 1', - reviewStatus: 'public', - dateSubmitted: '10 mins ago', - submittedBy: 'Alice Green', - }, - { - id: 'p2', - name: 'Public Review 2', - reviewStatus: 'public', - dateSubmitted: '33 mins ago', - submittedBy: 'Ben Carter', - }, - { - id: 'p3', - name: 'Public Review 3', - reviewStatus: 'public', - dateSubmitted: '2 hours ago', - submittedBy: 'Cara Kim', - }, - { - id: 'p4', - name: 'Public Review 4', - reviewStatus: 'public', - dateSubmitted: '1 day ago', - submittedBy: 'David Lin', - }, - { - id: 'p5', - name: 'Public Review 5', - reviewStatus: 'public', - dateSubmitted: '1 week ago', - submittedBy: 'Ella Nguyen', - }, -]; - -export const recentActivity = [ - { - id: 'p1', - name: 'Public Review 1', - reviewStatus: 'pending', - dateSubmitted: new Date('2023-10-01T12:00:00Z'), - submittedBy: 'Alice Green', - }, - { - id: 'p2', - name: 'Public Review 2', - reviewStatus: 'pending', - dateSubmitted: new Date('2025-06-04T12:00:00Z'), - submittedBy: 'Ben Carter', - }, - { - id: 'p3', - name: 'Public Review 3', - reviewStatus: 'rejected', - dateSubmitted: new Date('2025-06-02T12:00:00Z'), - submittedBy: 'Cara Kim', - }, - { - id: 'p4', - name: 'Public Review 4', - reviewStatus: 'accepted', - dateSubmitted: new Date('2025-04-01T12:00:00Z'), - submittedBy: 'David Lin', - }, - { - id: 'p5', - name: 'Public Review 5', - reviewStatus: 'rejected', - dateSubmitted: new Date('2024-06-01T12:00:00Z'), - submittedBy: 'Ella Nguyen', - }, -]; diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html index 2dbdc2029..798c7dcec 100644 --- a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html +++ b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html @@ -11,7 +11,7 @@ {{ 'moderation.submissionReview.accepted' | translate }} } } - {{ action.dateCreated | timeAgo }} + {{ action.dateCreated | dateAgo }} {{ 'moderation.submissionReview.by' | translate }} {{ action.createdBy }}

diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts index 3cc58affc..8099eaedf 100644 --- a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts +++ b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts @@ -15,13 +15,13 @@ import { CollectionsModerationSelectors, CreateCollectionSubmissionAction, } from '@osf/features/moderation/store/collections-moderation'; +import { DateAgoPipe } from '@osf/shared/pipes'; import { ModerationDecisionFormControls, ModerationSubmitType } from '@shared/enums'; -import { TimeAgoPipe } from '@shared/pipes/time-ago.pipe'; import { CollectionsSelectors } from '@shared/stores'; @Component({ selector: 'osf-make-decision-dialog', - imports: [Button, TranslatePipe, TimeAgoPipe, FormsModule, RadioButton, ReactiveFormsModule, Textarea], + imports: [Button, TranslatePipe, DateAgoPipe, FormsModule, RadioButton, ReactiveFormsModule, Textarea], templateUrl: './make-decision-dialog.component.html', styleUrl: './make-decision-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/pipes/time-ago.pipe.ts b/src/app/shared/pipes/time-ago.pipe.ts deleted file mode 100644 index f1195b17e..000000000 --- a/src/app/shared/pipes/time-ago.pipe.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'timeAgo', -}) -export class TimeAgoPipe implements PipeTransform { - transform(value: Date | string): string { - if (!value) { - return ''; - } - - const date = new Date(value); - const now = new Date(); - const seconds = Math.floor((now.getTime() - date.getTime()) / 1000); - - const minute = 60; - const hour = minute * 60; - const day = hour * 24; - const month = day * 30; - const year = day * 365; - - if (seconds < minute) { - return seconds === 0 ? 'just now' : `${seconds} seconds ago`; - } else if (seconds < hour) { - const minutes = Math.floor(seconds / minute); - return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; - } else if (seconds < day) { - const hours = Math.floor(seconds / hour); - return `${hours} hour${hours > 1 ? 's' : ''} ago`; - } else if (seconds < month) { - const days = Math.floor(seconds / day); - return `${days} day${days > 1 ? 's' : ''} ago`; - } else if (seconds < year) { - const months = Math.floor(seconds / month); - return `${months} month${months > 1 ? 's' : ''} ago`; - } else { - const years = Math.floor(seconds / year); - return `${years} year${years > 1 ? 's' : ''} ago`; - } - } -}