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.scss b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.scss index baf8accf8..e69de29bb 100644 --- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.scss +++ b/src/app/features/moderation/components/collection-submission-item/collection-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/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/index.ts b/src/app/features/moderation/components/index.ts index b328db41e..42b5e359e 100644 --- a/src/app/features/moderation/components/index.ts +++ b/src/app/features/moderation/components/index.ts @@ -7,8 +7,13 @@ 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'; +export { RegistrySubmissionItemComponent } from './registry-submission-item/registry-submission-item.component'; export { RegistrySubmissionsComponent } from './registry-submissions/registry-submissions.component'; export { CollectionSubmissionItemComponent } from '@osf/features/moderation/components/collection-submission-item/collection-submission-item.component'; export { CollectionSubmissionsListComponent } from '@osf/features/moderation/components/collection-submissions-list/collection-submissions-list.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..9dc694473 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; + + +
+ + + @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.html b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html new file mode 100644 index 000000000..bf9e01f67 --- /dev/null +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.html @@ -0,0 +1,61 @@ +
+
+ + + +

{{ 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..e76b06997 --- /dev/null +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.ts @@ -0,0 +1,127 @@ +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, Router } 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 router = inject(Router); + 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.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 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 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.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..d94bab9db --- /dev/null +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.html @@ -0,0 +1,47 @@ +
+ + +
+ + + @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) { +
+ {{ registryActionLabel[action.toState] | translate }} + {{ action.dateModified | dateAgo }} + {{ 'moderation.submissionReview.by' | translate }} + {{ action.creator.name }} + + @if (action.toState === registryActionState.Accepted) { + {{ '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..e69de29bb 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..7b47314e8 --- /dev/null +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts @@ -0,0 +1,36 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { IconComponent } from '@osf/shared/components'; +import { DateAgoPipe } from '@osf/shared/pipes'; + +import { REGISTRY_ACTION_LABEL, ReviewStatusIcon } from '../../constants'; +import { ActionStatus, SubmissionReviewStatus } from '../../enums'; +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 { + status = input.required(); + submission = input.required(); + + readonly reviewStatusIcon = ReviewStatusIcon; + readonly registryActionLabel = REGISTRY_ACTION_LABEL; + readonly registryActionState = ActionStatus; + + 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 dd993ea4a..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,14 +3,14 @@ -

{{ totalCount }}

{{ item.label | translate | titlecase }}

@@ -23,8 +23,39 @@ [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..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 @@ -1,15 +1,8 @@ -.pending { - color: var(--yellow-1); +.submission-container { + border: 1px solid var(--grey-2); + border-radius: 0.5rem; } -.accepted { - color: var(--green-1); -} - -.rejected { - color: var(--red-1); -} - -.withdrawn { - color: var(--dark-blue-1); +.submission-item:not(:last-child) { + border-bottom: 1px solid var(--grey-2); } 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 97a0e1df0..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 @@ -1,42 +1,124 @@ +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, 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 { 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 { 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', - imports: [SelectButton, TranslatePipe, FormsModule, SelectComponent, IconComponent, TitleCasePipe], + imports: [ + SelectButton, + TranslatePipe, + FormsModule, + SelectComponent, + 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 router = inject(Router); + 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.getStatusFromQueryParams(); + this.fetchSubmissions(); + } + + changeReviewStatus(value: SubmissionReviewStatus): void { + this.selectedReviewOption.set(value); + this.router.navigate([], { + relativeTo: this.route, + queryParams: { status: value }, + queryParamsHandling: 'merge', + }); - sortOptions = ALL_SORT_OPTIONS; - selectedSortOption = signal(null); - selectedReviewOption = this.submissionReviewOptions[0].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(); + } - totalCount = 5; + private getStatusFromQueryParams() { + const queryParams = this.route.snapshot.queryParams; + const statusValues = Object.values(SubmissionReviewStatus); - submissions = pubicReviews; + const statusParam = queryParams['status']; - changeReviewStatus(value: SubmissionReviewStatus) { - console.log(value); + if (statusParam && statusValues.includes(statusParam)) { + this.selectedReviewOption.set(statusParam); + } } - 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.selectedReviewOption(), + this.currentPage(), + this.selectedSortOption() + ); } } 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/features/moderation/constants/index.ts b/src/app/features/moderation/constants/index.ts index 32498e13f..f10dea5d8 100644 --- a/src/app/features/moderation/constants/index.ts +++ b/src/app/features/moderation/constants/index.ts @@ -1,7 +1,11 @@ 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'; export * from './submission.const'; export * from './upload-limits.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 new file mode 100644 index 000000000..9edd3d2e2 --- /dev/null +++ b/src/app/features/moderation/constants/registry-action-labels.const.ts @@ -0,0 +1,9 @@ +import { ActionStatus } from '../enums'; + +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 new file mode 100644 index 000000000..7eccf69a8 --- /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.sortOption.titleAZ', + }, + { + value: RegistrySort.TitleZA, + label: 'moderation.sortOption.titleZA', + }, + { + value: RegistrySort.RegisteredNewest, + label: 'moderation.sortOption.oldest', + }, + { + value: RegistrySort.RegisteredOldest, + 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 7063974b3..839fce93f 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.Removed, + 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, - icon: 'fas fa-lock', + 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', }, { @@ -40,12 +66,30 @@ export const SUBMITTED_SUBMISSION_REVIEW_OPTIONS = [ label: 'moderation.submissionReviewStatus.rejected', }, { - value: SubmissionReviewStatus.Withdrawn, + value: SubmissionReviewStatus.Removed, icon: 'fas fa-circle-minus', label: 'moderation.submissionReviewStatus.withdrawn', }, ]; +export const PENDING_SUBMISSION_REVIEW_OPTIONS: SubmissionReviewOption[] = [ + { + value: SubmissionReviewStatus.Pending, + icon: 'fas fa-hourglass', + label: 'moderation.submissionReviewStatus.pending', + }, + { + 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.Pending]: { value: SubmissionReviewStatus.Pending, @@ -69,10 +113,18 @@ 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 new file mode 100644 index 000000000..704773cdf --- /dev/null +++ b/src/app/features/moderation/mappers/registry-moderation.mapper.ts @@ -0,0 +1,44 @@ +import { PaginatedData } from '@osf/shared/models'; + +import { + RegistryDataJsonApi, + RegistryModeration, + RegistryResponseJsonApi, + ReviewAction, + ReviewActionsDataJsonApi, +} 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: ReviewActionsDataJsonApi): ReviewAction { + 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 d061124c0..5512ecd99 100644 --- a/src/app/features/moderation/models/index.ts +++ b/src/app/features/moderation/models/index.ts @@ -9,4 +9,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 './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-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..963bee893 --- /dev/null +++ b/src/app/features/moderation/models/registry-moderation.model.ts @@ -0,0 +1,11 @@ +import { ReviewAction } from './review-action.model'; + +export interface RegistryModeration { + id: string; + title: string; + reviewsState: string; + public: boolean; + embargoed: boolean; + embargoEndDate?: string; + actions: ReviewAction[]; +} diff --git a/src/app/features/moderation/models/review-action-json-api.model.ts b/src/app/features/moderation/models/review-action-json-api.model.ts new file mode 100644 index 000000000..253cdff57 --- /dev/null +++ b/src/app/features/moderation/models/review-action-json-api.model.ts @@ -0,0 +1,36 @@ +import { JsonApiResponse } from '@osf/core/models'; + +export type ReviewActionsResponseJsonApi = JsonApiResponse; + +export interface ReviewActionsDataJsonApi { + id: string; + attributes: ReviewActionAttributesJsonApi; + embeds: ReviewActionEmbedsJsonApi; +} + +interface ReviewActionAttributesJsonApi { + auto: boolean; + comment: string; + date_created: string; + date_modified: string; + from_state: string; + to_state: string; + trigger: string; + visible: true; +} + +interface ReviewActionEmbedsJsonApi { + creator: { + data: UserModelJsonApi; + }; +} + +interface UserModelJsonApi { + id: string; + type: 'users'; + attributes: UserAttributesJsonApi; +} + +interface UserAttributesJsonApi { + full_name: string; +} diff --git a/src/app/features/moderation/models/review-action.model.ts b/src/app/features/moderation/models/review-action.model.ts new file mode 100644 index 000000000..7d3242588 --- /dev/null +++ b/src/app/features/moderation/models/review-action.model.ts @@ -0,0 +1,10 @@ +import { IdName } from '@osf/shared/models'; + +export interface ReviewAction { + id: string; + fromState: string; + toState: string; + dateModified: string; + creator: IdName; + comment: 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 a254b9aa4..03d96c0c3 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/registry-moderation.routes.ts b/src/app/features/moderation/registry-moderation.routes.ts index d4c98106a..930c502e5 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/moderators'; +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/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 new file mode 100644 index 000000000..b59a9752d --- /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 { RegistryModeration, RegistryResponseJsonApi, ReviewAction, ReviewActionsResponseJsonApi } 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/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/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/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/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/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`; - } - } -} 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 bedea2735..bacb6181e 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -40,6 +40,7 @@ "view": "View", "review": "Review", "upload": "Upload", + "hide": "Hide", "customize": "Customize", "createCustomCitation": "Create Custom Citation", "preview": "Preview", @@ -1170,7 +1171,9 @@ "rejected": "Rejected", "withdrawn": "Withdrawn", "public": "Public", - "embargo": "Embargo" + "embargo": "Embargo", + "pendingUpdates": "Pending Updates", + "pendingWithdrawal": "Pending Withdrawal" }, "makeDecision": { "header": "Make decision", @@ -1192,6 +1195,7 @@ "submitted": "Submitted", "accepted": "Accepted", "rejected": "Rejected", + "requested": "Requested", "withdrawn": "Withdrawn", "by": "by" }, @@ -1227,6 +1231,20 @@ "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 accepted", + "registrySubmitted": "Registration submitted", + "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", + "showHistory": "Show history", + "sortOption": { + "titleAZ": "Title: A-Z", + "titleZA": "Title: Z-A", + "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 5231609c7..09fd79b4c 100644 --- a/src/assets/styles/overrides/button.scss +++ b/src/assets/styles/overrides/button.scss @@ -66,8 +66,6 @@ } .link-btn-no-padding { - .p-button { - --p-button-padding-y: 0; - --p-button-padding-x: 0; - } + --p-button-padding-y: 0; + --p-button-padding-x: 0; }