diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts
index 6aecdf081..a4415dad9 100644
--- a/src/app/core/components/nav-menu/nav-menu.component.ts
+++ b/src/app/core/components/nav-menu/nav-menu.component.ts
@@ -44,6 +44,7 @@ export class NavMenuComponent {
isProject: this.isProjectRoute() && !this.isRegistryRoute() && !this.isPreprintRoute(),
isRegistry: this.isRegistryRoute(),
isPreprint: this.isPreprintRoute(),
+ preprintReviewsPageVisible: this.canUserViewReviews(),
isCollections: this.isCollectionsRoute() || false,
currentUrl: this.router.url,
};
@@ -69,6 +70,7 @@ export class NavMenuComponent {
protected readonly isCollectionsRoute = computed(() => this.currentRoute().isCollectionsWithId);
protected readonly isRegistryRoute = computed(() => this.currentRoute().isRegistryRoute);
protected readonly isPreprintRoute = computed(() => this.currentRoute().isPreprintRoute);
+ protected readonly canUserViewReviews = select(UserSelectors.getCanViewReviews);
private getRouteInfo() {
const urlSegments = this.router.url.split('/').filter((segment) => segment);
diff --git a/src/app/core/helpers/nav-menu.helper.ts b/src/app/core/helpers/nav-menu.helper.ts
index 924e6616a..178988118 100644
--- a/src/app/core/helpers/nav-menu.helper.ts
+++ b/src/app/core/helpers/nav-menu.helper.ts
@@ -140,6 +140,9 @@ function updatePreprintMenuItem(item: MenuItem, ctx: RouteContext): MenuItem {
}
return { ...subItem, visible: false, expanded: false };
}
+ if (subItem.id === 'preprints-moderation') {
+ return { ...subItem, visible: ctx.preprintReviewsPageVisible };
+ }
return subItem;
});
diff --git a/src/app/core/models/route-context.model.ts b/src/app/core/models/route-context.model.ts
index 9507f5eb7..780ac929b 100644
--- a/src/app/core/models/route-context.model.ts
+++ b/src/app/core/models/route-context.model.ts
@@ -4,6 +4,7 @@ export interface RouteContext {
isProject: boolean;
isRegistry: boolean;
isPreprint: boolean;
+ preprintReviewsPageVisible?: boolean;
isCollections: boolean;
currentUrl?: string;
}
diff --git a/src/app/core/store/user/user.selectors.ts b/src/app/core/store/user/user.selectors.ts
index 9552da4ca..88ab9dd9d 100644
--- a/src/app/core/store/user/user.selectors.ts
+++ b/src/app/core/store/user/user.selectors.ts
@@ -63,6 +63,11 @@ export class UserSelectors {
return !!state.currentUser.data?.isModerator;
}
+ @Selector([UserState])
+ static getCanViewReviews(state: UserStateModel): boolean {
+ return state.currentUser.data?.canViewReviews || false;
+ }
+
@Selector([UserState])
static isAuthenticated(state: UserStateModel): boolean {
return !!state.currentUser.data || !!localStorage.getItem('currentUser');
diff --git a/src/app/features/moderation/components/moderators-list/moderators-list.component.html b/src/app/features/moderation/components/moderators-list/moderators-list.component.html
index 63d1c64bb..93c78d295 100644
--- a/src/app/features/moderation/components/moderators-list/moderators-list.component.html
+++ b/src/app/features/moderation/components/moderators-list/moderators-list.component.html
@@ -1,24 +1,26 @@
-
-
-
+
-
+ @if (isCurrentUserAdminModerator()) {
+
+ }
+ />
diff --git a/src/app/features/moderation/components/moderators-list/moderators-list.component.ts b/src/app/features/moderation/components/moderators-list/moderators-list.component.ts
index 3581085f5..5c575a75e 100644
--- a/src/app/features/moderation/components/moderators-list/moderators-list.component.ts
+++ b/src/app/features/moderation/components/moderators-list/moderators-list.component.ts
@@ -7,12 +7,23 @@ import { DialogService } from 'primeng/dynamicdialog';
import { debounceTime, distinctUntilChanged, filter, forkJoin, map, of, skip } from 'rxjs';
-import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, Signal, signal } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ DestroyRef,
+ effect,
+ inject,
+ OnInit,
+ Signal,
+ signal,
+} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
-import { AddModeratorType } from '@osf/features/moderation/enums';
+import { UserSelectors } from '@core/store/user';
+import { AddModeratorType, ModeratorPermission } from '@osf/features/moderation/enums';
import { ModeratorDialogAddModel, ModeratorModel } from '@osf/features/moderation/models';
import {
AddModerator,
@@ -58,6 +69,17 @@ export class ModeratorsListComponent implements OnInit {
moderators = signal([]);
initialModerators = select(ModeratorsSelectors.getModerators);
isModeratorsLoading = select(ModeratorsSelectors.isModeratorsLoading);
+ currentUser = select(UserSelectors.getCurrentUser);
+
+ isCurrentUserAdminModerator = computed(() => {
+ const currentUserId = this.currentUser()?.id;
+ const initialModerators = this.initialModerators();
+ if (!currentUserId) return false;
+
+ return initialModerators.some((moderator: ModeratorModel) => {
+ return moderator.userId === currentUserId && moderator.permission === ModeratorPermission.Admin;
+ });
+ });
protected actions = createDispatchMap({
loadModerators: LoadModerators,
diff --git a/src/app/features/moderation/components/moderators-table/moderators-table.component.html b/src/app/features/moderation/components/moderators-table/moderators-table.component.html
index 4b5556514..6930ab39c 100644
--- a/src/app/features/moderation/components/moderators-table/moderators-table.component.html
+++ b/src/app/features/moderation/components/moderators-table/moderators-table.component.html
@@ -38,14 +38,25 @@
-
+ @if (isCurrentUserAdminModerator()) {
+
+ } @else {
+ @switch (item.permission) {
+ @case (ModeratorPermission.Admin) {
+ {{ 'moderation.moderatorPermissions.administrator' | translate }}
+ }
+ @case (ModeratorPermission.Moderator) {
+ {{ 'moderation.moderatorPermissions.moderator' | translate }}
+ }
+ }
+ }
|
@@ -79,8 +90,16 @@
|
-
-
+ @if (isCurrentUserAdminModerator() || currentUserId() === item.id) {
+
+ }
|
} @else {
diff --git a/src/app/features/moderation/components/moderators-table/moderators-table.component.ts b/src/app/features/moderation/components/moderators-table/moderators-table.component.ts
index af7bcccad..1cfed3308 100644
--- a/src/app/features/moderation/components/moderators-table/moderators-table.component.ts
+++ b/src/app/features/moderation/components/moderators-table/moderators-table.component.ts
@@ -9,6 +9,7 @@ import { ChangeDetectionStrategy, Component, inject, input, output, signal } fro
import { FormsModule } from '@angular/forms';
import { MODERATION_PERMISSIONS } from '@osf/features/moderation/constants';
+import { ModeratorPermission } from '@osf/features/moderation/enums';
import { ModeratorModel } from '@osf/features/moderation/models';
import {
EducationHistoryDialogComponent,
@@ -29,6 +30,8 @@ import { TableParameters } from '@osf/shared/models';
export class ModeratorsTableComponent {
items = input([]);
isLoading = input(false);
+ currentUserId = input.required();
+ isCurrentUserAdminModerator = input.required();
update = output();
remove = output();
@@ -38,6 +41,7 @@ export class ModeratorsTableComponent {
protected readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS });
protected readonly permissionsOptions = MODERATION_PERMISSIONS;
+ protected readonly ModeratorPermission = ModeratorPermission;
skeletonData: ModeratorModel[] = Array.from({ length: 3 }, () => ({}) as ModeratorModel);
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
index fef358003..6d76bd71e 100644
--- a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts
+++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts
@@ -113,7 +113,7 @@ export class PreprintSubmissionsComponent implements OnInit {
}
navigateToPreprint(item: PreprintSubmission) {
- this.router.navigate(['/preprints/', item.id, 'overview'], { queryParams: { mode: 'moderator' } });
+ this.router.navigate(['/preprints/', this.providerId(), item.id], { queryParams: { mode: 'moderator' } });
}
private getStatusFromQueryParams() {
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
index c5d2da37c..a0b6fcd3e 100644
--- 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
@@ -110,7 +110,7 @@ export class PreprintWithdrawalSubmissionsComponent implements OnInit {
}
navigateToPreprint(item: PreprintWithdrawalSubmission) {
- this.router.navigate(['/preprints/', item.preprintId, 'overview'], { queryParams: { mode: 'moderator' } });
+ this.router.navigate(['/preprints/', this.providerId(), item.preprintId], { queryParams: { mode: 'moderator' } });
}
private getStatusFromQueryParams() {
diff --git a/src/app/features/moderation/constants/submission.const.ts b/src/app/features/moderation/constants/submission.const.ts
index 9ded27f00..377a9a34d 100644
--- a/src/app/features/moderation/constants/submission.const.ts
+++ b/src/app/features/moderation/constants/submission.const.ts
@@ -65,13 +65,13 @@ export const WITHDRAWAL_SUBMISSION_REVIEW_OPTIONS: SubmissionReviewOption[] = [
{
value: SubmissionReviewStatus.Accepted,
icon: 'fas fa-circle-check',
- label: 'moderation.submissionReviewStatus.accepted',
+ label: 'moderation.submissionReviewStatus.approved',
count: 0,
},
{
value: SubmissionReviewStatus.Rejected,
icon: 'fas fa-circle-minus',
- label: 'moderation.submissionReviewStatus.rejected',
+ label: 'moderation.submissionReviewStatus.declined',
count: 0,
},
];
diff --git a/src/app/features/moderation/models/index.ts b/src/app/features/moderation/models/index.ts
index 897e8984a..302a37ead 100644
--- a/src/app/features/moderation/models/index.ts
+++ b/src/app/features/moderation/models/index.ts
@@ -17,5 +17,4 @@ 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/submission.model.ts b/src/app/features/moderation/models/submission.model.ts
deleted file mode 100644
index f29815543..000000000
--- a/src/app/features/moderation/models/submission.model.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface CollectionSubmission {
- id: string;
- title: string;
- reviewStatus: string;
- dateSubmitted: string;
- submittedBy: string;
-}
diff --git a/src/app/features/moderation/services/moderators.service.ts b/src/app/features/moderation/services/moderators.service.ts
index 4db0f2ab6..9c554e713 100644
--- a/src/app/features/moderation/services/moderators.service.ts
+++ b/src/app/features/moderation/services/moderators.service.ts
@@ -21,7 +21,7 @@ export class ModeratorsService {
private readonly urlMap = new Map([
[ResourceType.Collection, 'providers/collections'],
[ResourceType.Registration, 'providers/registrations'],
- [ResourceType.Preprint, 'preprint_providers'],
+ [ResourceType.Preprint, 'providers/preprints'],
]);
getModerators(resourceId: string, resourceType: ResourceType): Observable {
diff --git a/src/app/features/moderation/services/preprint-moderation.service.ts b/src/app/features/moderation/services/preprint-moderation.service.ts
index 3dce5ceac..2e827bb68 100644
--- a/src/app/features/moderation/services/preprint-moderation.service.ts
+++ b/src/app/features/moderation/services/preprint-moderation.service.ts
@@ -29,7 +29,7 @@ export class PreprintModerationService {
private readonly jsonApiService = inject(JsonApiService);
getPreprintProviders(): Observable {
- const baseUrl = `${environment.apiUrl}/preprint_providers/?filter[permissions]=view_actions,set_up_moderation`;
+ const baseUrl = `${environment.apiUrl}/providers/preprints/?filter[permissions]=view_actions,set_up_moderation`;
return this.jsonApiService
.get>(baseUrl)
diff --git a/src/app/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts
index 2a063528f..9f9ae08df 100644
--- a/src/app/features/preprints/components/index.ts
+++ b/src/app/features/preprints/components/index.ts
@@ -6,6 +6,7 @@ export { PreprintsInstitutionFilterComponent } from './filters/preprints-institu
export { PreprintsLicenseFilterComponent } from './filters/preprints-license-filter/preprints-license-filter.component';
export { AdditionalInfoComponent } from './preprint-details/additional-info/additional-info.component';
export { GeneralInformationComponent } from './preprint-details/general-information/general-information.component';
+export { ModerationStatusBannerComponent } from './preprint-details/moderation-status-banner/moderation-status-banner.component';
export { PreprintFileSectionComponent } from './preprint-details/preprint-file-section/preprint-file-section.component';
export { ShareAndDownloadComponent } from './preprint-details/share-and-downlaod/share-and-download.component';
export { StatusBannerComponent } from './preprint-details/status-banner/status-banner.component';
@@ -19,6 +20,8 @@ export { PreprintsFilterChipsComponent } from '@osf/features/preprints/component
export { PreprintsResourcesComponent } from '@osf/features/preprints/components/filters/preprints-resources/preprints-resources.component';
export { PreprintsResourcesFiltersComponent } from '@osf/features/preprints/components/filters/preprints-resources-filters/preprints-resources-filters.component';
export { PreprintsSubjectFilterComponent } from '@osf/features/preprints/components/filters/preprints-subject-filter/preprints-subject-filter.component';
+export { MakeDecisionComponent } from '@osf/features/preprints/components/preprint-details/make-decision/make-decision.component';
+export { PreprintTombstoneComponent } from '@osf/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component';
export { WithdrawDialogComponent } from '@osf/features/preprints/components/preprint-details/withdraw-dialog/withdraw-dialog.component';
export { FileStepComponent } from '@osf/features/preprints/components/stepper/file-step/file-step.component';
export { MetadataStepComponent } from '@osf/features/preprints/components/stepper/metadata-step/metadata-step.component';
diff --git a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html
index 1408e18e3..5accbda77 100644
--- a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html
+++ b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.html
@@ -13,12 +13,22 @@ {{ 'preprints.preprintStepper.review.sections.metadata.publicationCitation'
@if (preprintValue.originalPublicationDate) {
- {{ 'preprints.preprintStepper.review.sections.metadata.publicationDate' | translate }}
+ {{ 'preprints.details.originalPublicationDate' | translate }}
{{ preprintValue.originalPublicationDate | date: 'MMM d, y, h:mm a' }}
}
+ @if (preprintValue.doi) {
+
+ }
+
{{ 'preprints.preprintStepper.review.sections.metadata.license' | translate }}
diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html
index 196f9e4e2..5b4ea4c1e 100644
--- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html
+++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.html
@@ -19,6 +19,16 @@ {{ 'preprints.preprintStepper.review.sections.metadata.affiliatedInstitution
}
+ @if (preprintValue.nodeId) {
+
+ }
+
{{ 'preprints.preprintStepper.review.sections.metadata.authors' | translate }}
@@ -108,7 +118,10 @@
}
-
+
}
diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts
index 842cfe221..757e4d53e 100644
--- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts
+++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts
@@ -5,21 +5,33 @@ import { TranslatePipe } from '@ngx-translate/core';
import { Card } from 'primeng/card';
import { Skeleton } from 'primeng/skeleton';
-import { ChangeDetectionStrategy, Component, computed, effect, input, OnDestroy, signal } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, effect, input, OnDestroy, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
+import { RouterLink } from '@angular/router';
import { PreprintDoiSectionComponent } from '@osf/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component';
import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums';
import { PreprintProviderDetails } from '@osf/features/preprints/models';
import { FetchPreprintById, PreprintSelectors } from '@osf/features/preprints/store/preprint';
-import { TruncatedTextComponent } from '@shared/components';
+import { IconComponent, TruncatedTextComponent } from '@shared/components';
import { ResourceType } from '@shared/enums';
import { Institution } from '@shared/models';
import { ContributorsSelectors, GetAllContributors, ResetContributorsState } from '@shared/stores';
+import { environment } from 'src/environments/environment';
+
@Component({
selector: 'osf-preprint-general-information',
- imports: [Card, TranslatePipe, TruncatedTextComponent, Skeleton, FormsModule, PreprintDoiSectionComponent],
+ imports: [
+ Card,
+ TranslatePipe,
+ TruncatedTextComponent,
+ Skeleton,
+ FormsModule,
+ PreprintDoiSectionComponent,
+ RouterLink,
+ IconComponent,
+ ],
templateUrl: './general-information.component.html',
styleUrl: './general-information.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -33,7 +45,10 @@ export class GeneralInformationComponent implements OnDestroy {
resetContributorsState: ResetContributorsState,
fetchPreprintById: FetchPreprintById,
});
+ protected readonly environment = environment;
+
preprintProvider = input.required();
+ preprintVersionSelected = output();
preprint = select(PreprintSelectors.getPreprint);
isPreprintLoading = select(PreprintSelectors.isPreprintLoading);
@@ -49,6 +64,10 @@ export class GeneralInformationComponent implements OnDestroy {
skeletonData = Array.from({ length: 5 }, () => null);
+ nodeLink = computed(() => {
+ return `${environment.webUrl}/${this.preprint()?.nodeId}`;
+ });
+
constructor() {
effect(() => {
const preprint = this.preprint();
diff --git a/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.html b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.html
new file mode 100644
index 000000000..9c4cb5bbd
--- /dev/null
+++ b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.html
@@ -0,0 +1,148 @@
+
+
+
+
+ @if (isPendingWithdrawal()) {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (didValidate() && decision() === ReviewsState.Rejected) {
+
+ {{ requestDecisionJustificationErrorMessage() }}
+
+ }
+
+
+
+ } @else {
+ @if (preprint()?.reviewsState === ReviewsState.Withdrawn) {
+ {{ latestAction()!.comment }}
+ } @else {
+
+
+ - {{ settingsComments() | translate }}
+ @if (!provider().reviewsCommentsPrivate) {
+ - {{ settingsNames() | translate }}
+ }
+ - {{ settingsModeration() | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (commentExceedsLimit()) {
+
+ {{ commentLengthErrorMessage() }}
+
+ }
+
+
+
+ @if (preprint()?.reviewsState !== ReviewsState.Pending) {
+
+ }
+
+
+ }
+ }
+
+
diff --git a/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.scss b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts
new file mode 100644
index 000000000..54701ba1e
--- /dev/null
+++ b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MakeDecisionComponent } from './make-decision.component';
+
+describe.skip('MakeDecisionComponent', () => {
+ let component: MakeDecisionComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [MakeDecisionComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(MakeDecisionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.ts b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.ts
new file mode 100644
index 000000000..33ce709b6
--- /dev/null
+++ b/src/app/features/preprints/components/preprint-details/make-decision/make-decision.component.ts
@@ -0,0 +1,316 @@
+import { createDispatchMap, select } from '@ngxs/store';
+
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { Dialog } from 'primeng/dialog';
+import { Message } from 'primeng/message';
+import { RadioButton } from 'primeng/radiobutton';
+import { Textarea } from 'primeng/textarea';
+import { Tooltip } from 'primeng/tooltip';
+
+import { TitleCasePipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { ReviewAction } from '@osf/features/moderation/models';
+import { decisionExplanation, decisionSettings, formInputLimits } from '@osf/features/preprints/constants';
+import { ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums';
+import { PreprintProviderDetails, PreprintRequest } from '@osf/features/preprints/models';
+import {
+ PreprintSelectors,
+ SubmitRequestsDecision,
+ SubmitReviewsDecision,
+} from '@osf/features/preprints/store/preprint';
+import { StringOrNull } from '@shared/helpers';
+
+@Component({
+ selector: 'osf-make-decision',
+ imports: [Button, TranslatePipe, TitleCasePipe, Dialog, Tooltip, RadioButton, FormsModule, Textarea, Message],
+ templateUrl: './make-decision.component.html',
+ styleUrl: './make-decision.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MakeDecisionComponent {
+ private readonly translateService = inject(TranslateService);
+ private readonly router = inject(Router);
+ private readonly actions = createDispatchMap({
+ submitReviewsDecision: SubmitReviewsDecision,
+ submitRequestsDecision: SubmitRequestsDecision,
+ });
+
+ protected readonly ReviewsState = ReviewsState;
+
+ preprint = select(PreprintSelectors.getPreprint);
+ provider = input.required();
+ latestAction = input.required();
+ latestWithdrawalRequest = input.required();
+ isPendingWithdrawal = input.required();
+
+ dialogVisible = false;
+ didValidate = signal(false);
+ decision = signal(ReviewsState.Accepted);
+ initialReviewerComment = signal(null);
+ reviewerComment = signal(null);
+ requestDecisionJustification = signal(null);
+ saving = signal(false);
+
+ labelDecisionButton = computed(() => {
+ const preprint = this.preprint()!;
+ if (preprint.reviewsState === ReviewsState.Withdrawn) {
+ return 'preprints.details.decision.withdrawalReason';
+ } else if (this.isPendingWithdrawal()) {
+ return 'preprints.details.decision.makeDecision';
+ } else {
+ return preprint.reviewsState === ReviewsState.Pending
+ ? 'preprints.details.decision.makeDecision'
+ : 'preprints.details.decision.modifyDecision';
+ }
+ });
+
+ makeDecisionButtonDisabled = computed(() => {
+ const reason = this.latestAction()?.comment;
+ const state = this.preprint()?.reviewsState;
+ return state === ReviewsState.Withdrawn && !reason;
+ });
+
+ labelDecisionDialogHeader = computed(() => {
+ const preprint = this.preprint()!;
+
+ if (preprint.reviewsState === ReviewsState.Withdrawn) {
+ return 'preprints.details.decision.header.withdrawalReason';
+ } else if (this.isPendingWithdrawal()) {
+ return 'preprints.details.decision.header.submitDecision';
+ } else {
+ return preprint.reviewsState === ReviewsState.Pending
+ ? 'preprints.details.decision.header.submitDecision'
+ : 'preprints.details.decision.header.modifyDecision';
+ }
+ });
+
+ labelSubmitButton = computed(() => {
+ if (this.isPendingWithdrawal()) {
+ return 'preprints.details.decision.submitButton.submitDecision';
+ } else if (this.preprint()?.reviewsState === ReviewsState.Pending) {
+ return 'preprints.details.decision.submitButton.submitDecision';
+ } else if (this.decisionChanged()) {
+ return 'preprints.details.decision.submitButton.modifyDecision';
+ } else if (this.commentEdited()) {
+ return 'preprints.details.decision.submitButton.updateComment';
+ }
+ return 'preprints.details.decision.submitButton.modifyDecision';
+ });
+
+ submitButtonDisabled = computed(() => {
+ return (!this.decisionChanged() && !this.commentEdited()) || this.commentExceedsLimit();
+ });
+
+ acceptOptionExplanation = computed(() => {
+ const reviewsWorkflow = this.provider().reviewsWorkflow;
+ if (reviewsWorkflow === ProviderReviewsWorkflow.PreModeration) {
+ return 'preprints.details.decision.accept.pre';
+ } else if (reviewsWorkflow === ProviderReviewsWorkflow.PostModeration) {
+ return 'preprints.details.decision.accept.post';
+ }
+
+ return 'preprints.details.decision.accept.pre';
+ });
+
+ rejectOptionLabel = computed(() => {
+ return this.preprint()?.isPublished
+ ? 'preprints.details.decision.withdrawn.label'
+ : 'preprints.details.decision.reject.label';
+ });
+
+ labelRequestDecisionJustification = computed(() => {
+ if (this.decision() === ReviewsState.Accepted) {
+ return 'preprints.details.decision.withdrawalJustification';
+ } else if (this.decision() === ReviewsState.Rejected) {
+ return 'preprints.details.decision.denialJustification';
+ }
+
+ return 'preprints.details.decision.withdrawalJustification';
+ });
+
+ rejectOptionExplanation = computed(() => {
+ const reviewsWorkflow = this.provider().reviewsWorkflow;
+ if (reviewsWorkflow === ProviderReviewsWorkflow.PreModeration) {
+ if (this.preprint()?.reviewsState === ReviewsState.Accepted) {
+ return 'preprints.details.decision.approve.explanation';
+ } else {
+ return decisionExplanation.reject[reviewsWorkflow];
+ }
+ } else {
+ return decisionExplanation.withdrawn[reviewsWorkflow!];
+ }
+ });
+
+ rejectRadioButtonValue = computed(() => {
+ return this.preprint()?.isPublished ? ReviewsState.Withdrawn : ReviewsState.Rejected;
+ });
+
+ settingsComments = computed(() => {
+ const commentType = this.provider().reviewsCommentsPrivate ? 'private' : 'public';
+ return decisionSettings.comments[commentType];
+ });
+
+ settingsNames = computed(() => {
+ const commentType = this.provider().reviewsCommentsAnonymous ? 'anonymous' : 'named';
+ return decisionSettings.names[commentType];
+ });
+
+ settingsModeration = computed(() => {
+ return decisionSettings.moderation[this.provider().reviewsWorkflow || ProviderReviewsWorkflow.PreModeration];
+ });
+
+ commentEdited = computed(() => {
+ return this.reviewerComment()?.trim() !== this.initialReviewerComment();
+ });
+
+ commentExceedsLimit = computed(() => {
+ const comment = this.reviewerComment();
+ if (!comment) return false;
+
+ return comment.length > formInputLimits.decisionComment.maxLength;
+ });
+
+ commentLengthErrorMessage = computed(() => {
+ const limit = formInputLimits.decisionComment.maxLength;
+ return this.translateService.instant('preprints.details.decision.commentLengthError', {
+ limit,
+ difference: Math.abs(limit - this.reviewerComment()!.length).toString(),
+ });
+ });
+
+ requestDecisionJustificationErrorMessage = computed(() => {
+ const justification = this.requestDecisionJustification();
+ const minLength = formInputLimits.requestDecisionJustification.minLength;
+
+ if (!justification) return this.translateService.instant('preprints.details.decision.justificationRequiredError');
+ if (justification.length < minLength)
+ return this.translateService.instant('preprints.details.decision.justificationLengthError', {
+ minLength,
+ });
+
+ return null;
+ });
+
+ decisionChanged = computed(() => {
+ return this.preprint()?.reviewsState !== this.decision();
+ });
+
+ constructor() {
+ effect(() => {
+ const preprint = this.preprint();
+ const latestAction = this.latestAction();
+ if (preprint && latestAction) {
+ if (preprint.reviewsState === ReviewsState.Pending) {
+ this.decision.set(ReviewsState.Accepted);
+ this.initialReviewerComment.set(null);
+ this.reviewerComment.set(null);
+ } else {
+ this.decision.set(preprint.reviewsState);
+ this.initialReviewerComment.set(latestAction?.comment);
+ this.reviewerComment.set(latestAction?.comment);
+ }
+ }
+ });
+
+ effect(() => {
+ const withdrawalRequest = this.latestWithdrawalRequest();
+ if (!withdrawalRequest) return;
+
+ this.requestDecisionJustification.set(withdrawalRequest.comment);
+ });
+ }
+
+ submit() {
+ // don't remove comments
+ const preprint = this.preprint()!;
+ let trigger = '';
+ if (preprint.reviewsState !== ReviewsState.Pending && this.commentEdited() && !this.decisionChanged()) {
+ // If the submission is not pending,
+ // the decision has not changed and the comment is edited.
+ // the trigger would be 'edit_comment'
+ trigger = 'edit_comment';
+ } else {
+ let actionType = '';
+ if (preprint.isPublished && this.isPendingWithdrawal()) {
+ // if the submission is published and is pending withdrawal.
+ // actionType would be 'reject'
+ // meaning moderators could accept/reject the withdrawl request
+ actionType = 'reject';
+ } else if (preprint.isPublished && !this.isPendingWithdrawal()) {
+ // if the submission is published and is not pending withdrawal
+ // actionType would be 'withdraw'
+ // meaning moderators could approve/directly withdraw the submission
+ actionType = 'withdraw';
+ } else {
+ // Otherwise
+ // actionType would be 'reject'
+ // meaning the moderator could either accept or reject the submission
+ actionType = 'reject';
+ }
+ // If the decision is to accept the submission or the withdrawal request,
+ // the trigger is 'accept'
+ // If not, then the trigger is whatever 'actionType' set above.
+ trigger = this.decision() === ReviewsState.Accepted ? 'accept' : actionType;
+ }
+
+ let comment: StringOrNull = '';
+ if (this.isPendingWithdrawal()) {
+ if (trigger === 'reject') {
+ this.didValidate.set(true);
+ if (this.requestDecisionJustificationErrorMessage() !== null) {
+ return;
+ }
+ }
+
+ comment = this.requestDecisionJustification()?.trim() || null;
+ } else {
+ comment = this.reviewerComment()?.trim() || null;
+ }
+
+ this.saving.set(true);
+ if (this.isPendingWithdrawal()) {
+ this.actions.submitRequestsDecision(this.latestWithdrawalRequest()!.id, trigger, comment).subscribe({
+ next: () => {
+ this.saving.set(false);
+ this.router.navigate(['preprints', this.provider().id, 'moderation', 'withdrawals']);
+ },
+ error: () => {
+ this.saving.set(false);
+ },
+ });
+ } else {
+ this.actions.submitReviewsDecision(trigger, comment).subscribe({
+ next: () => {
+ this.saving.set(false);
+ this.router.navigate(['preprints', this.provider().id, 'moderation', 'submissions']);
+ },
+ error: () => {
+ this.saving.set(false);
+ },
+ });
+ }
+ }
+
+ requestDecisionToggled() {
+ if (!this.isPendingWithdrawal()) {
+ return;
+ }
+
+ if (this.decision() === ReviewsState.Accepted) {
+ this.requestDecisionJustification.set(this.latestWithdrawalRequest()?.comment || null);
+ } else if (this.decision() === ReviewsState.Rejected) {
+ this.requestDecisionJustification.set(null);
+ }
+ }
+
+ cancel() {
+ this.dialogVisible = false;
+ this.decision.set(this.preprint()!.reviewsState);
+ this.reviewerComment.set(this.initialReviewerComment());
+ }
+}
diff --git a/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.html b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.html
new file mode 100644
index 000000000..7c74d37e4
--- /dev/null
+++ b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
{{ status() | translate | titlecase }}:
+
+ @if (noActions()) {
+
+ {{ recentActivityLanguage() | translate: { documentType: documentType()?.singular } }}
+ {{ labelDate() | date: 'MMM d, y' }}
+
+ } @else {
+
+ {{ actionCreatorName() }}
+ {{ recentActivityLanguage() | translate: { documentType: documentType()?.singular } }}
+ {{ labelDate() | date: 'MMM d, y' }}
+
+ }
+
+ @if (isPendingWithdrawal()) {
+
{{ withdrawalRequesterName() }}
+ {{ requestActivityLanguage()! | translate: { documentType: documentType()?.singular } }}
+ {{ latestWithdrawalRequest()?.dateLastTransitioned | date: 'MMM d, y' }}
+ }
+
+
+
+
+
diff --git a/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.scss b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts
new file mode 100644
index 000000000..5ce1214d5
--- /dev/null
+++ b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ModerationStatusBannerComponent } from './moderation-status-banner.component';
+
+describe.skip('ModerationStatusBannerComponent', () => {
+ let component: ModerationStatusBannerComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ModerationStatusBannerComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ModerationStatusBannerComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.ts b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.ts
new file mode 100644
index 000000000..129bce73b
--- /dev/null
+++ b/src/app/features/preprints/components/preprint-details/moderation-status-banner/moderation-status-banner.component.ts
@@ -0,0 +1,125 @@
+import { select } from '@ngxs/store';
+
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+
+import { Message } from 'primeng/message';
+
+import { DatePipe, TitleCasePipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
+
+import { ReviewAction } from '@osf/features/moderation/models';
+import {
+ recentActivityMessageByState,
+ statusIconByState,
+ statusLabelKeyByState,
+ statusSeverityByState,
+ statusSeverityByWorkflow,
+} from '@osf/features/preprints/constants';
+import { ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums';
+import { getPreprintDocumentType } from '@osf/features/preprints/helpers';
+import { PreprintProviderDetails, PreprintRequest } from '@osf/features/preprints/models';
+import { PreprintSelectors } from '@osf/features/preprints/store/preprint';
+import { IconComponent } from '@shared/components';
+
+import { environment } from 'src/environments/environment';
+
+@Component({
+ selector: 'osf-moderation-status-banner',
+ imports: [IconComponent, Message, TitleCasePipe, TranslatePipe, DatePipe],
+ templateUrl: './moderation-status-banner.component.html',
+ styleUrl: './moderation-status-banner.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ModerationStatusBannerComponent {
+ private readonly translateService = inject(TranslateService);
+ protected readonly environment = environment;
+
+ preprint = select(PreprintSelectors.getPreprint);
+ provider = input.required();
+ latestAction = input.required();
+ latestWithdrawalRequest = input.required();
+
+ isPendingWithdrawal = input.required();
+ noActions = computed(() => {
+ return this.latestAction() === null;
+ });
+
+ documentType = computed(() => {
+ const provider = this.provider();
+ if (!provider) return null;
+
+ return getPreprintDocumentType(provider, this.translateService);
+ });
+
+ labelDate = computed(() => {
+ const preprint = this.preprint()!;
+ return preprint.dateWithdrawn ? preprint.dateWithdrawn : preprint.dateLastTransitioned;
+ });
+
+ status = computed(() => {
+ const currentState = this.preprint()!.reviewsState;
+
+ if (this.isPendingWithdrawal()) {
+ return statusLabelKeyByState[ReviewsState.Pending]!;
+ } else {
+ return statusLabelKeyByState[currentState]!;
+ }
+ });
+
+ iconClass = computed(() => {
+ const currentState = this.preprint()!.reviewsState;
+
+ if (this.isPendingWithdrawal()) {
+ return statusIconByState[ReviewsState.Pending];
+ }
+
+ return statusIconByState[currentState];
+ });
+
+ severity = computed(() => {
+ const currentState = this.preprint()!.reviewsState;
+
+ if (this.isPendingWithdrawal()) {
+ return statusSeverityByState[ReviewsState.Pending];
+ } else {
+ return currentState === ReviewsState.Pending
+ ? statusSeverityByWorkflow[this.provider()?.reviewsWorkflow as ProviderReviewsWorkflow]
+ : statusSeverityByState[currentState];
+ }
+ });
+
+ recentActivityLanguage = computed(() => {
+ const currentState = this.preprint()!.reviewsState;
+
+ if (this.noActions()) {
+ return recentActivityMessageByState.automatic[currentState]!;
+ } else {
+ return recentActivityMessageByState[currentState]!;
+ }
+ });
+
+ requestActivityLanguage = computed(() => {
+ if (!this.isPendingWithdrawal()) {
+ return;
+ }
+
+ return recentActivityMessageByState[ReviewsState.PendingWithdrawal];
+ });
+
+ actionCreatorName = computed(() => {
+ return this.latestAction()?.creator.name;
+ });
+ actionCreatorLink = computed(() => {
+ return `${environment.webUrl}/${this.actionCreatorId()}`;
+ });
+ actionCreatorId = computed(() => {
+ return this.latestAction()?.creator.id;
+ });
+
+ withdrawalRequesterName = computed(() => {
+ return this.latestWithdrawalRequest()?.creator.name;
+ });
+ withdrawalRequesterId = computed(() => {
+ return this.latestWithdrawalRequest()?.creator.id;
+ });
+}
diff --git a/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.ts b/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.ts
index 43f4e9110..869822f1f 100644
--- a/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.ts
+++ b/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.ts
@@ -1,16 +1,14 @@
-import { createDispatchMap, select } from '@ngxs/store';
+import { select } from '@ngxs/store';
import { TranslatePipe } from '@ngx-translate/core';
import { Select } from 'primeng/select';
-import { Location } from '@angular/common';
-import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
-import { Router } from '@angular/router';
import { PreprintProviderDetails } from '@osf/features/preprints/models';
-import { FetchPreprintById, PreprintSelectors } from '@osf/features/preprints/store/preprint';
+import { PreprintSelectors } from '@osf/features/preprints/store/preprint';
@Component({
selector: 'osf-preprint-doi-section',
@@ -20,16 +18,11 @@ import { FetchPreprintById, PreprintSelectors } from '@osf/features/preprints/st
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PreprintDoiSectionComponent {
- private readonly router = inject(Router);
- private readonly location = inject(Location);
-
- private actions = createDispatchMap({
- fetchPreprintById: FetchPreprintById,
- });
-
preprintProvider = input.required();
preprint = select(PreprintSelectors.getPreprint);
+ preprintVersionSelected = output();
+
preprintVersionIds = select(PreprintSelectors.getPreprintVersionIds);
arePreprintVersionIdsLoading = select(PreprintSelectors.arePreprintVersionIdsLoading);
@@ -46,13 +39,6 @@ export class PreprintDoiSectionComponent {
selectPreprintVersion(versionId: string) {
if (this.preprint()!.id === versionId) return;
- this.actions.fetchPreprintById(versionId).subscribe({
- complete: () => {
- const currentUrl = this.router.url;
- const newUrl = currentUrl.replace(/[^/]+$/, versionId);
-
- this.location.replaceState(newUrl);
- },
- });
+ this.preprintVersionSelected.emit(versionId);
}
}
diff --git a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html
index 84d2a0efa..56b0e8d3a 100644
--- a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html
+++ b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.html
@@ -2,6 +2,13 @@
@if (preprint()) {
@let preprintValue = preprint()!;
+ @if (preprintValue.withdrawalJustification) {
+
+ {{ 'preprints.details.reasonForWithdrawal' | translate }}
+ {{ preprintValue.withdrawalJustification }}
+
+ }
+
{{ 'preprints.preprintStepper.review.sections.metadata.authors' | translate }}
@@ -24,7 +31,10 @@ {{ 'preprints.preprintStepper.common.labels.abstract' | translate }}
-
+
{{ 'preprints.preprintStepper.review.sections.metadata.license' | translate }}
diff --git a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts
index 30ab75ebd..a41200bee 100644
--- a/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts
+++ b/src/app/features/preprints/components/preprint-details/preprint-tombstone/preprint-tombstone.component.ts
@@ -8,7 +8,7 @@ import { Skeleton } from 'primeng/skeleton';
import { Tag } from 'primeng/tag';
import { DatePipe } from '@angular/common';
-import { ChangeDetectionStrategy, Component, computed, effect, input, OnDestroy } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, effect, input, OnDestroy, output } from '@angular/core';
import { PreprintDoiSectionComponent } from '@osf/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component';
import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums';
@@ -55,6 +55,8 @@ export class PreprintTombstoneComponent implements OnDestroy {
fetchPreprintById: FetchPreprintById,
fetchSubjects: FetchSelectedSubjects,
});
+ preprintVersionSelected = output();
+
preprintProvider = input.required();
preprint = select(PreprintSelectors.getPreprint);
diff --git a/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.ts b/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.ts
index c60c0db96..cb6ddfde5 100644
--- a/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.ts
+++ b/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.ts
@@ -20,7 +20,7 @@ import {
statusSeverityByWorkflow,
} from '@osf/features/preprints/constants';
import { ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums';
-import { PreprintProviderDetails } from '@osf/features/preprints/models';
+import { PreprintProviderDetails, PreprintRequestAction } from '@osf/features/preprints/models';
import { PreprintSelectors } from '@osf/features/preprints/store/preprint';
import { IconComponent } from '@shared/components';
@@ -39,6 +39,7 @@ export class StatusBannerComponent {
latestAction = input.required();
isPendingWithdrawal = input.required();
isWithdrawalRejected = input.required();
+ latestRequestAction = input.required();
feedbackDialogVisible = false;
@@ -83,11 +84,19 @@ export class StatusBannerComponent {
});
reviewerName = computed(() => {
- return this.latestAction()?.creator.name;
+ if (this.isWithdrawalRejected()) {
+ return this.latestRequestAction()?.creator.name;
+ } else {
+ return this.latestAction()?.creator.name;
+ }
});
reviewerComment = computed(() => {
- return this.latestAction()?.comment;
+ if (this.isWithdrawalRejected()) {
+ return this.latestRequestAction()?.comment;
+ } else {
+ return this.latestAction()?.comment;
+ }
});
isWithdrawn = computed(() => {
diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html
index a6cee4c1a..84f8d707e 100644
--- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html
+++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.html
@@ -58,12 +58,13 @@ {{ preprintProvider()!.name }}
}
-
@if (isPreprintProviderLoading()) {
- } @else {
+ } @else if (preprintProvider()?.examplePreprintId) {
- {{ 'preprints.showExample' | translate }}
+ {{ 'preprints.showExample' | translate }}
+
}
diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.ts b/src/app/features/preprints/components/stepper/review-step/review-step.component.ts
index ee80a5345..e4a131298 100644
--- a/src/app/features/preprints/components/stepper/review-step/review-step.component.ts
+++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.ts
@@ -11,7 +11,7 @@ import { DatePipe, TitleCasePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, inject, input, OnInit, signal } from '@angular/core';
import { Router } from '@angular/router';
-import { ApplicabilityStatus, PreregLinkInfo } from '@osf/features/preprints/enums';
+import { ApplicabilityStatus, PreregLinkInfo, ReviewsState } from '@osf/features/preprints/enums';
import { PreprintProviderDetails } from '@osf/features/preprints/models';
import {
FetchLicenses,
@@ -83,12 +83,16 @@ export class ReviewStepComponent implements OnInit {
}
submitPreprint() {
- this.actions.submitPreprint().subscribe({
- complete: () => {
- this.toastService.showSuccess('preprints.preprintStepper.common.successMessages.preprintSubmitted');
- this.router.navigate(['/preprints', this.provider()!.id, this.preprint()!.id]);
- },
- });
+ if (this.preprint()?.reviewsState !== ReviewsState.Accepted) {
+ this.actions.submitPreprint().subscribe({
+ complete: () => {
+ this.toastService.showSuccess('preprints.preprintStepper.common.successMessages.preprintSubmitted');
+ this.router.navigate(['/preprints', this.provider()!.id, this.preprint()!.id]);
+ },
+ });
+ } else {
+ this.toastService.showSuccess('preprints.preprintStepper.common.successMessages.preprintSubmitted');
+ }
}
cancelSubmission() {
diff --git a/src/app/features/preprints/constants/form-input-limits.const.ts b/src/app/features/preprints/constants/form-input-limits.const.ts
index ab0596bb2..2fb0de2d2 100644
--- a/src/app/features/preprints/constants/form-input-limits.const.ts
+++ b/src/app/features/preprints/constants/form-input-limits.const.ts
@@ -15,4 +15,10 @@ export const formInputLimits = {
withdrawalJustification: {
minLength: 25,
},
+ decisionComment: {
+ maxLength: 100,
+ },
+ requestDecisionJustification: {
+ minLength: 20,
+ },
};
diff --git a/src/app/features/preprints/constants/index.ts b/src/app/features/preprints/constants/index.ts
index fb8a0f186..936d1f7f4 100644
--- a/src/app/features/preprints/constants/index.ts
+++ b/src/app/features/preprints/constants/index.ts
@@ -1,5 +1,6 @@
export * from './create-new-version-steps.const';
export * from './form-input-limits.const';
+export * from './make-decision.const';
export * from './preprints-fields.const';
export * from './prereg-link-options.const';
export * from './status-banner.const';
diff --git a/src/app/features/preprints/constants/make-decision.const.ts b/src/app/features/preprints/constants/make-decision.const.ts
new file mode 100644
index 000000000..04f1d3377
--- /dev/null
+++ b/src/app/features/preprints/constants/make-decision.const.ts
@@ -0,0 +1,30 @@
+import { ProviderReviewsWorkflow } from '@osf/features/preprints/enums';
+
+export const decisionSettings = {
+ comments: {
+ public: 'preprints.details.decision.settings.comments.public',
+ private: 'preprints.details.decision.settings.comments.private',
+ },
+ names: {
+ anonymous: 'preprints.details.decision.settings.names.anonymous',
+ named: 'preprints.details.decision.settings.names.named',
+ },
+ moderation: {
+ [ProviderReviewsWorkflow.PreModeration]: 'preprints.details.decision.settings.moderation.pre',
+ [ProviderReviewsWorkflow.PostModeration]: 'preprints.details.decision.settings.moderation.post',
+ },
+};
+
+export const decisionExplanation = {
+ accept: {
+ [ProviderReviewsWorkflow.PreModeration]: 'preprints.details.decision.accept.pre',
+ [ProviderReviewsWorkflow.PostModeration]: 'preprints.details.decision.accept.post',
+ },
+ reject: {
+ [ProviderReviewsWorkflow.PreModeration]: 'preprints.details.decision.reject.pre',
+ [ProviderReviewsWorkflow.PostModeration]: 'preprints.details.decision.reject.post',
+ },
+ withdrawn: {
+ [ProviderReviewsWorkflow.PostModeration]: 'preprints.details.decision.withdrawn.post',
+ },
+};
diff --git a/src/app/features/preprints/constants/status-banner.const.ts b/src/app/features/preprints/constants/status-banner.const.ts
index 61da85340..fa87c962d 100644
--- a/src/app/features/preprints/constants/status-banner.const.ts
+++ b/src/app/features/preprints/constants/status-banner.const.ts
@@ -17,7 +17,7 @@ export const statusIconByState: Partial> = {
[ReviewsState.Rejected]: 'times-circle',
[ReviewsState.PendingWithdrawal]: 'hourglass',
[ReviewsState.WithdrawalRejected]: 'times-circle',
- [ReviewsState.Withdrawn]: 'exclamation-triangle',
+ [ReviewsState.Withdrawn]: 'circle-minus',
};
export const statusMessageByWorkflow: Record = {
@@ -44,4 +44,21 @@ export const statusSeverityByState: Partial
[ReviewsState.PendingWithdrawal]: 'error',
[ReviewsState.WithdrawalRejected]: 'error',
[ReviewsState.Withdrawn]: 'warn',
+ [ReviewsState.Pending]: 'warn',
+};
+
+type ActivityMap = Partial>;
+
+export const recentActivityMessageByState: ActivityMap & {
+ automatic: ActivityMap;
+} = {
+ [ReviewsState.Pending]: 'preprints.details.moderationStatusBanner.recentActivity.pending',
+ [ReviewsState.Accepted]: 'preprints.details.moderationStatusBanner.recentActivity.accepted',
+ [ReviewsState.Rejected]: 'preprints.details.moderationStatusBanner.recentActivity.rejected',
+ [ReviewsState.PendingWithdrawal]: 'preprints.details.moderationStatusBanner.recentActivity.pendingWithdrawal',
+ [ReviewsState.Withdrawn]: 'preprints.details.moderationStatusBanner.recentActivity.withdrawn',
+ automatic: {
+ [ReviewsState.Pending]: 'preprints.details.moderationStatusBanner.recentActivity.automatic.pending',
+ [ReviewsState.Accepted]: 'preprints.details.moderationStatusBanner.recentActivity.automatic.accepted',
+ },
};
diff --git a/src/app/features/preprints/mappers/index.ts b/src/app/features/preprints/mappers/index.ts
index c9fbba01e..18f9a8804 100644
--- a/src/app/features/preprints/mappers/index.ts
+++ b/src/app/features/preprints/mappers/index.ts
@@ -1,3 +1,4 @@
export * from './preprint-providers.mapper';
export * from './preprint-request.mapper';
+export * from './preprint-request-actions.mapper';
export * from './preprints.mapper';
diff --git a/src/app/features/preprints/mappers/preprint-providers.mapper.ts b/src/app/features/preprints/mappers/preprint-providers.mapper.ts
index b9fecd7fe..4f74aeae4 100644
--- a/src/app/features/preprints/mappers/preprint-providers.mapper.ts
+++ b/src/app/features/preprints/mappers/preprint-providers.mapper.ts
@@ -19,6 +19,7 @@ export class PreprintProvidersMapper {
preprintWord: response.attributes.preprint_word,
allowSubmissions: response.attributes.allow_submissions,
assertionsEnabled: response.attributes.assertions_enabled,
+ permissions: response.attributes.permissions,
brand: {
id: brandRaw.id,
name: brandRaw.attributes.name,
diff --git a/src/app/features/preprints/mappers/preprint-request-actions.mapper.ts b/src/app/features/preprints/mappers/preprint-request-actions.mapper.ts
new file mode 100644
index 000000000..62bbc1477
--- /dev/null
+++ b/src/app/features/preprints/mappers/preprint-request-actions.mapper.ts
@@ -0,0 +1,39 @@
+import { PreprintRequestAction, PreprintRequestActionDataJsonApi } from '@osf/features/preprints/models';
+import { StringOrNull } from '@shared/helpers';
+
+export class PreprintRequestActionsMapper {
+ static fromPreprintRequestActions(data: PreprintRequestActionDataJsonApi): PreprintRequestAction {
+ return {
+ id: data.id,
+ trigger: data.attributes.trigger,
+ comment: data.attributes.comment,
+ fromState: data.attributes.from_state,
+ toState: data.attributes.to_state,
+ dateModified: data.attributes.date_modified,
+ creator: {
+ id: data.embeds.creator.data.id,
+ name: data.embeds.creator.data.attributes.full_name,
+ },
+ };
+ }
+
+ static toRequestActionPayload(requestId: string, trigger: string, comment: StringOrNull) {
+ return {
+ data: {
+ type: 'preprint_request_actions',
+ attributes: {
+ trigger,
+ ...(comment && { comment }),
+ },
+ relationships: {
+ target: {
+ data: {
+ type: 'preprint-requests',
+ id: requestId,
+ },
+ },
+ },
+ },
+ };
+ }
+}
diff --git a/src/app/features/preprints/mappers/preprint-request.mapper.ts b/src/app/features/preprints/mappers/preprint-request.mapper.ts
index cee3612b1..48877ae38 100644
--- a/src/app/features/preprints/mappers/preprint-request.mapper.ts
+++ b/src/app/features/preprints/mappers/preprint-request.mapper.ts
@@ -28,6 +28,11 @@ export class PreprintRequestMapper {
comment: data.attributes.comment,
requestType: data.attributes.request_type,
machineState: data.attributes.machine_state,
+ dateLastTransitioned: data.attributes.date_last_transitioned,
+ creator: {
+ id: data.embeds.creator.data.id,
+ name: data.embeds.creator.data.attributes.full_name,
+ },
};
}
}
diff --git a/src/app/features/preprints/mappers/preprints.mapper.ts b/src/app/features/preprints/mappers/preprints.mapper.ts
index f182b6f3c..317d881ea 100644
--- a/src/app/features/preprints/mappers/preprints.mapper.ts
+++ b/src/app/features/preprints/mappers/preprints.mapper.ts
@@ -1,5 +1,6 @@
import { LicensesMapper } from '@osf/shared/mappers';
import { ApiData, JsonApiResponseWithMeta, ResponseJsonApi } from '@osf/shared/models';
+import { StringOrNull } from '@shared/helpers';
import {
Preprint,
@@ -41,6 +42,7 @@ export class PreprintsMapper {
dateModified: response.attributes.date_modified,
dateWithdrawn: response.attributes.date_withdrawn,
datePublished: response.attributes.date_published,
+ dateLastTransitioned: response.attributes.date_last_transitioned,
title: response.attributes.title,
description: response.attributes.description,
reviewsState: response.attributes.reviews_state,
@@ -51,6 +53,7 @@ export class PreprintsMapper {
originalPublicationDate: response.attributes.original_publication_date,
isPublished: response.attributes.is_published,
tags: response.attributes.tags,
+ withdrawalJustification: response.attributes.withdrawal_justification,
isPublic: response.attributes.public,
version: response.attributes.version,
isLatestVersion: response.attributes.is_latest_version,
@@ -94,6 +97,7 @@ export class PreprintsMapper {
dateModified: data.attributes.date_modified,
dateWithdrawn: data.attributes.date_withdrawn,
datePublished: data.attributes.date_published,
+ dateLastTransitioned: data.attributes.date_last_transitioned,
title: data.attributes.title,
description: data.attributes.description,
reviewsState: data.attributes.reviews_state,
@@ -103,6 +107,7 @@ export class PreprintsMapper {
customPublicationCitation: data.attributes.custom_publication_citation,
originalPublicationDate: data.attributes.original_publication_date,
isPublished: data.attributes.is_published,
+ withdrawalJustification: data.attributes.withdrawal_justification,
tags: data.attributes.tags,
isPublic: data.attributes.public,
version: data.attributes.version,
@@ -136,12 +141,13 @@ export class PreprintsMapper {
};
}
- static toSubmitPreprintPayload(preprintId: string) {
+ static toReviewActionPayload(preprintId: string, trigger: string, comment?: StringOrNull) {
return {
data: {
type: 'review_actions',
attributes: {
- trigger: 'submit',
+ trigger,
+ ...(comment && { comment }),
},
relationships: {
target: {
diff --git a/src/app/features/preprints/models/index.ts b/src/app/features/preprints/models/index.ts
index 3cc0d9423..9131b75cb 100644
--- a/src/app/features/preprints/models/index.ts
+++ b/src/app/features/preprints/models/index.ts
@@ -4,5 +4,7 @@ export * from './preprint-licenses-json-api.models';
export * from './preprint-provider.models';
export * from './preprint-provider-json-api.models';
export * from './preprint-request.models';
+export * from './preprint-request-action.models';
+export * from './preprint-request-action-json-api.models';
export * from './preprint-request-json-api.models';
export * from './submit-preprint-form.models';
diff --git a/src/app/features/preprints/models/preprint-json-api.models.ts b/src/app/features/preprints/models/preprint-json-api.models.ts
index 936595b4c..858555abf 100644
--- a/src/app/features/preprints/models/preprint-json-api.models.ts
+++ b/src/app/features/preprints/models/preprint-json-api.models.ts
@@ -9,6 +9,9 @@ export interface PreprintAttributesJsonApi {
date_modified: string;
date_published: Date | null;
original_publication_date: Date | null;
+ date_last_transitioned: Date | null;
+ date_withdrawn: Date | null;
+ withdrawal_justification: StringOrNull;
custom_publication_citation: StringOrNull;
doi: StringOrNull;
preprint_doi_created: Date | null;
@@ -18,11 +21,9 @@ export interface PreprintAttributesJsonApi {
is_preprint_orphan: boolean;
license_record: LicenseRecordJsonApi | null;
tags: string[];
- date_withdrawn: Date | null;
current_user_permissions: UserPermissions[];
public: boolean;
reviews_state: ReviewsState;
- date_last_transitioned: Date | null;
version: number;
is_latest_version: boolean;
has_coi: BooleanOrNull;
diff --git a/src/app/features/preprints/models/preprint-provider-json-api.models.ts b/src/app/features/preprints/models/preprint-provider-json-api.models.ts
index fa568105b..7e92396c2 100644
--- a/src/app/features/preprints/models/preprint-provider-json-api.models.ts
+++ b/src/app/features/preprints/models/preprint-provider-json-api.models.ts
@@ -1,5 +1,6 @@
import { StringOrNull } from '@osf/shared/helpers';
-import { BrandDataJsonApi } from '@osf/shared/models';
+import { ReviewPermissions } from '@shared/enums/review-permissions.enum';
+import { BrandDataJsonApi } from '@shared/models';
import { ProviderReviewsWorkflow } from '../enums';
@@ -16,6 +17,7 @@ export interface PreprintProviderDetailsJsonApi {
domain: string;
footer_links: string;
preprint_word: PreprintWord;
+ permissions: ReviewPermissions[];
assets: {
wide_white: string;
square_color_no_transparent: string;
diff --git a/src/app/features/preprints/models/preprint-provider.models.ts b/src/app/features/preprints/models/preprint-provider.models.ts
index 0fbc718b5..bc60444a9 100644
--- a/src/app/features/preprints/models/preprint-provider.models.ts
+++ b/src/app/features/preprints/models/preprint-provider.models.ts
@@ -1,5 +1,6 @@
-import { StringOrNull } from '@osf/shared/helpers';
-import { Brand } from '@osf/shared/models';
+import { ReviewPermissions } from '@shared/enums/review-permissions.enum';
+import { StringOrNull } from '@shared/helpers';
+import { Brand } from '@shared/models';
import { ProviderReviewsWorkflow } from '../enums';
@@ -18,6 +19,7 @@ export interface PreprintProviderDetails {
allowSubmissions: boolean;
assertionsEnabled: boolean;
reviewsWorkflow: ProviderReviewsWorkflow | null;
+ permissions: ReviewPermissions[];
brand: Brand;
lastFetched?: number;
iri: string;
diff --git a/src/app/features/preprints/models/preprint-request-action-json-api.models.ts b/src/app/features/preprints/models/preprint-request-action-json-api.models.ts
new file mode 100644
index 000000000..311375c6a
--- /dev/null
+++ b/src/app/features/preprints/models/preprint-request-action-json-api.models.ts
@@ -0,0 +1,35 @@
+import { JsonApiResponse } from '@osf/shared/models';
+
+export type PreprintRequestActionsJsonApiResponse = JsonApiResponse;
+
+export interface PreprintRequestActionDataJsonApi {
+ id: string;
+ type: 'preprint_request_actions';
+ attributes: PreprintRequestActionsAttributesJsonApi;
+ embeds: PreprintRequestEmbedsJsonApi;
+}
+
+interface PreprintRequestActionsAttributesJsonApi {
+ trigger: string;
+ comment: string;
+ from_state: string;
+ to_state: string;
+ date_created: Date;
+ date_modified: Date;
+}
+
+interface PreprintRequestEmbedsJsonApi {
+ creator: {
+ data: UserModelJsonApi;
+ };
+}
+
+interface UserModelJsonApi {
+ id: string;
+ type: 'users';
+ attributes: UserAttributesJsonApi;
+}
+
+interface UserAttributesJsonApi {
+ full_name: string;
+}
diff --git a/src/app/features/preprints/models/preprint-request-action.models.ts b/src/app/features/preprints/models/preprint-request-action.models.ts
new file mode 100644
index 000000000..7fbdf8fee
--- /dev/null
+++ b/src/app/features/preprints/models/preprint-request-action.models.ts
@@ -0,0 +1,11 @@
+import { IdName } from '@shared/models';
+
+export interface PreprintRequestAction {
+ id: string;
+ trigger: string;
+ comment: string;
+ fromState: string;
+ toState: string;
+ dateModified: Date;
+ creator: IdName;
+}
diff --git a/src/app/features/preprints/models/preprint-request-json-api.models.ts b/src/app/features/preprints/models/preprint-request-json-api.models.ts
index 4a98f3cfc..ae8ad5d30 100644
--- a/src/app/features/preprints/models/preprint-request-json-api.models.ts
+++ b/src/app/features/preprints/models/preprint-request-json-api.models.ts
@@ -7,6 +7,7 @@ export interface PreprintRequestDataJsonApi {
id: string;
type: 'preprint_requests';
attributes: PreprintRequestAttributesJsonApi;
+ embeds: PreprintRequestEmbedsJsonApi;
}
interface PreprintRequestAttributesJsonApi {
@@ -17,3 +18,19 @@ interface PreprintRequestAttributesJsonApi {
modified: Date;
date_last_transitioned: Date;
}
+
+interface PreprintRequestEmbedsJsonApi {
+ creator: {
+ data: UserModelJsonApi;
+ };
+}
+
+interface UserModelJsonApi {
+ id: string;
+ type: 'users';
+ attributes: UserAttributesJsonApi;
+}
+
+interface UserAttributesJsonApi {
+ full_name: string;
+}
diff --git a/src/app/features/preprints/models/preprint-request.models.ts b/src/app/features/preprints/models/preprint-request.models.ts
index d7231abe8..9489fbaa4 100644
--- a/src/app/features/preprints/models/preprint-request.models.ts
+++ b/src/app/features/preprints/models/preprint-request.models.ts
@@ -1,8 +1,11 @@
import { PreprintRequestMachineState, PreprintRequestType } from '@osf/features/preprints/enums';
+import { IdName } from '@shared/models';
export interface PreprintRequest {
id: string;
comment: string;
machineState: PreprintRequestMachineState;
requestType: PreprintRequestType;
+ dateLastTransitioned: Date;
+ creator: IdName;
}
diff --git a/src/app/features/preprints/models/preprint.models.ts b/src/app/features/preprints/models/preprint.models.ts
index 8f2471609..11a7283bd 100644
--- a/src/app/features/preprints/models/preprint.models.ts
+++ b/src/app/features/preprints/models/preprint.models.ts
@@ -10,6 +10,7 @@ export interface Preprint {
dateModified: string;
dateWithdrawn: Date | null;
datePublished: Date | null;
+ dateLastTransitioned: Date | null;
title: string;
description: string;
reviewsState: ReviewsState;
@@ -24,6 +25,7 @@ export interface Preprint {
version: number;
isLatestVersion: boolean;
isPreprintOrphan: boolean;
+ withdrawalJustification: StringOrNull;
nodeId: StringOrNull;
primaryFileId: StringOrNull;
licenseId: StringOrNull;
diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.html b/src/app/features/preprints/pages/landing/preprints-landing.component.html
index a3ddcf395..13845ba84 100644
--- a/src/app/features/preprints/pages/landing/preprints-landing.component.html
+++ b/src/app/features/preprints/pages/landing/preprints-landing.component.html
@@ -35,11 +35,14 @@ {{ 'preprints.title' | translate }}
(triggerSearch)="redirectToSearchPageWithValue()"
/>
-
@if (isPreprintProviderLoading()) {
- } @else {
- {{ 'preprints.showExample' | translate }}
+ } @else if (osfPreprintProvider()!.examplePreprintId) {
+ {{ 'preprints.showExample' | translate }}
+
}
diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.html b/src/app/features/preprints/pages/preprint-details/preprint-details.component.html
index b135a1a31..56f3e4b58 100644
--- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.html
+++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.html
@@ -1,12 +1,34 @@
-
+
@if (isPreprintProviderLoading() || isPreprintLoading()) {
-
-
+
} @else {
-
![Provider Logo]()
-
{{ preprint()!.title }}
+
+
![Provider Logo]()
+
{{ preprint()!.title }}
+
+ }
+
+ @if (moderationMode()) {
+ @if (
+ isPreprintLoading() ||
+ areReviewActionsLoading() ||
+ areWithdrawalRequestsLoading() ||
+ areRequestActionsLoading()
+ ) {
+
+ } @else {
+
+ }
}
@@ -46,10 +68,19 @@
{{ preprint()!.title }}
+ @if (moderationStatusBannerVisible()) {
+
+ }
@if (statusBannerVisible()) {
@@ -63,12 +94,19 @@ {{ preprint()!.title }}
} @else {
-
+
}
diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts
index c37a570d6..51744791c 100644
--- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts
+++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts
@@ -8,7 +8,7 @@ import { Skeleton } from 'primeng/skeleton';
import { filter, map, of } from 'rxjs';
-import { DatePipe } from '@angular/common';
+import { DatePipe, Location } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@@ -26,28 +26,29 @@ import { UserSelectors } from '@core/store/user';
import {
AdditionalInfoComponent,
GeneralInformationComponent,
+ MakeDecisionComponent,
+ ModerationStatusBannerComponent,
PreprintFileSectionComponent,
+ PreprintTombstoneComponent,
ShareAndDownloadComponent,
StatusBannerComponent,
WithdrawDialogComponent,
} from '@osf/features/preprints/components';
-import { UserPermissions } from '@osf/shared/enums';
-import { IS_MEDIUM, pathJoin } from '@osf/shared/helpers';
-import { ContributorModel } from '@osf/shared/models';
-import { MetaTagsService } from '@osf/shared/services';
-import { ContributorsSelectors } from '@osf/shared/stores';
-
-import { PreprintTombstoneComponent } from '../../components/preprint-details/preprint-tombstone/preprint-tombstone.component';
-import { PreprintRequestMachineState, ProviderReviewsWorkflow, ReviewsState } from '../../enums';
+import { PreprintRequestMachineState, ProviderReviewsWorkflow, ReviewsState } from '@osf/features/preprints/enums';
import {
FetchPreprintById,
+ FetchPreprintRequestActions,
FetchPreprintRequests,
FetchPreprintReviewActions,
PreprintSelectors,
ResetState,
-} from '../../store/preprint';
-import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers';
-import { CreateNewVersion, PreprintStepperSelectors } from '../../store/preprint-stepper';
+} from '@osf/features/preprints/store/preprint';
+import { GetPreprintProviderById, PreprintProvidersSelectors } from '@osf/features/preprints/store/preprint-providers';
+import { CreateNewVersion, PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper';
+import { IS_MEDIUM, pathJoin } from '@osf/shared/helpers';
+import { ReviewPermissions, UserPermissions } from '@shared/enums';
+import { MetaTagsService } from '@shared/services';
+import { ContributorsSelectors } from '@shared/stores';
import { environment } from 'src/environments/environment';
@@ -63,6 +64,8 @@ import { environment } from 'src/environments/environment';
StatusBannerComponent,
TranslatePipe,
PreprintTombstoneComponent,
+ ModerationStatusBannerComponent,
+ MakeDecisionComponent,
],
templateUrl: './preprint-details.component.html',
styleUrl: './preprint-details.component.scss',
@@ -72,9 +75,10 @@ import { environment } from 'src/environments/environment';
export class PreprintDetailsComponent implements OnInit, OnDestroy {
@HostBinding('class') classes = 'flex-1 flex flex-column w-full';
+ private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
+ private readonly location = inject(Location);
private readonly store = inject(Store);
- private readonly router = inject(Router);
private readonly dialogService = inject(DialogService);
private readonly destroyRef = inject(DestroyRef);
private readonly translateService = inject(TranslateService);
@@ -92,8 +96,8 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
createNewVersion: CreateNewVersion,
fetchPreprintRequests: FetchPreprintRequests,
fetchPreprintReviewActions: FetchPreprintReviewActions,
+ fetchPreprintRequestActions: FetchPreprintRequestActions,
});
-
currentUser = select(UserSelectors.getCurrentUser);
preprintProvider = select(PreprintProvidersSelectors.getPreprintProviderDetails(this.providerId()));
isPreprintProviderLoading = select(PreprintProvidersSelectors.isPreprintProviderDetailsLoading);
@@ -105,6 +109,14 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
areReviewActionsLoading = select(PreprintSelectors.arePreprintReviewActionsLoading);
withdrawalRequests = select(PreprintSelectors.getPreprintRequests);
areWithdrawalRequestsLoading = select(PreprintSelectors.arePreprintRequestsLoading);
+ requestActions = select(PreprintSelectors.getPreprintRequestActions);
+ areRequestActionsLoading = select(PreprintSelectors.arePreprintRequestActionsLoading);
+
+ isPresentModeratorQueryParam = toSignal(this.route.queryParams.pipe(map((params) => params['mode'] === 'moderator')));
+ moderationMode = computed(() => {
+ const provider = this.preprintProvider();
+ return this.isPresentModeratorQueryParam() && provider?.permissions.includes(ReviewPermissions.ViewSubmissions);
+ });
latestAction = computed(() => {
const actions = this.reviewActions();
@@ -120,6 +132,13 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
return requests[0];
});
+ latestRequestAction = computed(() => {
+ const actions = this.requestActions();
+
+ if (actions.length < 1) return null;
+
+ return actions[0];
+ });
private currentUserIsAdmin = computed(() => {
return this.preprint()?.currentUserPermissions.includes(UserPermissions.Admin) || false;
@@ -127,18 +146,15 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
private currentUserIsContributor = computed(() => {
const contributors = this.contributors();
- const preprint = this.preprint()!;
const currentUser = this.currentUser();
if (this.currentUserIsAdmin()) {
return true;
} else if (contributors.length) {
- const authorIds = [] as string[];
- contributors.forEach((author: ContributorModel) => {
- authorIds.push(author.id);
- });
- const authorId = `${preprint.id}-${currentUser?.id}`;
- return currentUser?.id ? authorIds.includes(authorId) && this.hasReadWriteAccess() : false;
+ const authorIds = contributors.map((author) => author.id);
+ return currentUser?.id
+ ? authorIds.some((id) => id.endsWith(currentUser!.id)) && this.hasReadWriteAccess()
+ : false;
}
return false;
});
@@ -204,13 +220,13 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
});
isWithdrawalRejected = computed(() => {
- //[RNi] TODO: Implement when request actions available
- //const isPreprintRequestActionModel = this.args.latestAction instanceof PreprintRequestActionModel;
- // return isPreprintRequestActionModel && this.args.latestAction?.actionTrigger === 'reject';
- return false;
+ const latestRequestActions = this.latestRequestAction();
+ if (!latestRequestActions) return false;
+ return latestRequestActions?.trigger === 'reject';
});
withdrawalButtonVisible = computed(() => {
+ if (this.areWithdrawalRequestsLoading() || this.areRequestActionsLoading()) return false;
return (
this.currentUserIsAdmin() &&
this.preprintWithdrawableState() &&
@@ -219,10 +235,29 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
);
});
+ moderationStatusBannerVisible = computed(() => {
+ return (
+ this.moderationMode() &&
+ !(
+ this.isPreprintLoading() ||
+ this.areReviewActionsLoading() ||
+ this.areWithdrawalRequestsLoading() ||
+ this.areRequestActionsLoading()
+ )
+ );
+ });
+
statusBannerVisible = computed(() => {
const provider = this.preprintProvider();
const preprint = this.preprint();
- if (!provider || !preprint || this.areWithdrawalRequestsLoading() || this.areReviewActionsLoading()) return false;
+ if (
+ !provider ||
+ !preprint ||
+ this.areWithdrawalRequestsLoading() ||
+ this.areReviewActionsLoading() ||
+ this.areRequestActionsLoading()
+ )
+ return false;
return (
provider.reviewsWorkflow &&
@@ -234,14 +269,24 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
});
ngOnInit() {
- this.fetchPreprint();
- this.actions.getPreprintProviderById(this.providerId());
+ this.actions.getPreprintProviderById(this.providerId()).subscribe({
+ next: () => {
+ this.fetchPreprint(this.preprintId());
+ },
+ });
}
ngOnDestroy() {
this.actions.resetState();
}
+ fetchPreprintVersion(preprintVersionId: string) {
+ const currentUrl = this.router.url;
+ const newUrl = currentUrl.replace(/[^/]+$/, preprintVersionId);
+ this.location.replaceState(newUrl);
+ this.fetchPreprint(preprintVersionId);
+ }
+
handleWithdrawClicked() {
const dialogWidth = this.isMedium() ? '700px' : '340px';
@@ -262,7 +307,7 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
dialogRef.onClose.pipe(takeUntilDestroyed(this.destroyRef), filter(Boolean)).subscribe({
next: () => {
- this.fetchPreprint();
+ this.fetchPreprint(this.preprintId());
},
});
}
@@ -280,12 +325,21 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
});
}
- private fetchPreprint() {
- this.actions.fetchPreprintById(this.preprintId()).subscribe({
+ private fetchPreprint(preprintId: string) {
+ this.actions.fetchPreprintById(preprintId).subscribe({
next: () => {
- if (this.preprint()!.currentUserPermissions.length > 0) {
- this.actions.fetchPreprintRequests();
+ if (this.preprint()!.currentUserPermissions.length > 0 || this.moderationMode()) {
this.actions.fetchPreprintReviewActions();
+ if (this.preprintWithdrawableState() && (this.currentUserIsAdmin() || this.moderationMode())) {
+ this.actions.fetchPreprintRequests().subscribe({
+ next: () => {
+ const latestWithdrawalRequest = this.latestWithdrawalRequest();
+ if (latestWithdrawalRequest) {
+ this.actions.fetchPreprintRequestActions(latestWithdrawalRequest.id);
+ }
+ },
+ });
+ }
}
this.setMetaTags();
diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts
index dd472fe60..d52fde721 100644
--- a/src/app/features/preprints/services/preprints.service.ts
+++ b/src/app/features/preprints/services/preprints.service.ts
@@ -4,7 +4,9 @@ import { inject, Injectable } from '@angular/core';
import { RegistryModerationMapper } from '@osf/features/moderation/mappers';
import { ReviewActionsResponseJsonApi } from '@osf/features/moderation/models';
-import { searchPreferencesToJsonApiQueryParams } from '@osf/shared/helpers';
+import { PreprintRequestActionsMapper } from '@osf/features/preprints/mappers/preprint-request-actions.mapper';
+import { PreprintRequestAction } from '@osf/features/preprints/models/preprint-request-action.models';
+import { searchPreferencesToJsonApiQueryParams, StringOrNull } from '@osf/shared/helpers';
import { ApiData, JsonApiResponse, JsonApiResponseWithMeta, ResponseJsonApi, SearchFilters } from '@osf/shared/models';
import { JsonApiService } from '@osf/shared/services';
@@ -18,6 +20,7 @@ import {
PreprintMetaJsonApi,
PreprintRelationshipsJsonApi,
PreprintRequest,
+ PreprintRequestActionsJsonApiResponse,
PreprintRequestsJsonApiResponse,
} from '../models';
@@ -109,7 +112,7 @@ export class PreprintsService {
}
submitPreprint(preprintId: string) {
- const payload = PreprintsMapper.toSubmitPreprintPayload(preprintId);
+ const payload = PreprintsMapper.toReviewActionPayload(preprintId, 'submit');
return this.jsonApiService.post(`${environment.apiUrl}/preprints/${preprintId}/review_actions/`, payload);
}
@@ -167,16 +170,34 @@ export class PreprintsService {
}
getPreprintRequests(preprintId: string): Observable {
- const baseUrl = `${environment.apiUrl}/preprints/${preprintId}/requests/`;
+ const baseUrl = `${environment.apiUrl}/preprints/${preprintId}/requests/?embed=creator`;
return this.jsonApiService
.get(baseUrl)
.pipe(map((response) => response.data.map((x) => PreprintRequestMapper.fromPreprintRequest(x))));
}
+ getPreprintRequestActions(requestId: string): Observable {
+ const baseUrl = `${environment.apiUrl}/requests/${requestId}/actions/`;
+
+ return this.jsonApiService
+ .get(baseUrl)
+ .pipe(map((response) => response.data.map((x) => PreprintRequestActionsMapper.fromPreprintRequestActions(x))));
+ }
+
withdrawPreprint(preprintId: string, justification: string) {
const payload = PreprintRequestMapper.toWithdrawPreprintPayload(preprintId, justification);
return this.jsonApiService.post(`${environment.apiUrl}/preprints/${preprintId}/requests/`, payload);
}
+
+ submitReviewsDecision(preprintId: string, trigger: string, comment: StringOrNull) {
+ const payload = PreprintsMapper.toReviewActionPayload(preprintId, trigger, comment);
+ return this.jsonApiService.post(`${environment.apiUrl}/actions/reviews/`, payload);
+ }
+
+ submitRequestsDecision(requestId: string, trigger: string, comment: StringOrNull) {
+ const payload = PreprintRequestActionsMapper.toRequestActionPayload(requestId, trigger, comment);
+ return this.jsonApiService.post(`${environment.apiUrl}/actions/requests/preprints/`, payload);
+ }
}
diff --git a/src/app/features/preprints/store/preprint/preprint.actions.ts b/src/app/features/preprints/store/preprint/preprint.actions.ts
index f272a5428..5446d1966 100644
--- a/src/app/features/preprints/store/preprint/preprint.actions.ts
+++ b/src/app/features/preprints/store/preprint/preprint.actions.ts
@@ -1,3 +1,4 @@
+import { StringOrNull } from '@shared/helpers';
import { SearchFilters } from '@shared/models';
export class FetchMyPreprints {
@@ -36,6 +37,12 @@ export class FetchPreprintRequests {
static readonly type = '[Preprint] Fetch Preprint Requests';
}
+export class FetchPreprintRequestActions {
+ static readonly type = '[Preprint] Fetch Preprint Requests Actions';
+
+ constructor(public requestId: string) {}
+}
+
export class WithdrawPreprint {
static readonly type = '[Preprint] Withdraw Preprint';
@@ -45,6 +52,25 @@ export class WithdrawPreprint {
) {}
}
+export class SubmitReviewsDecision {
+ static readonly type = '[Preprint] Submit Reviews Decision';
+
+ constructor(
+ public trigger: string,
+ public comment: StringOrNull
+ ) {}
+}
+
+export class SubmitRequestsDecision {
+ static readonly type = '[Preprint] Submit Request Decision';
+
+ constructor(
+ public requestId: string,
+ public trigger: string,
+ public comment: StringOrNull
+ ) {}
+}
+
export class ResetState {
static readonly type = '[Preprint] Reset State';
}
diff --git a/src/app/features/preprints/store/preprint/preprint.model.ts b/src/app/features/preprints/store/preprint/preprint.model.ts
index f66df3963..e59f0c9de 100644
--- a/src/app/features/preprints/store/preprint/preprint.model.ts
+++ b/src/app/features/preprints/store/preprint/preprint.model.ts
@@ -1,5 +1,5 @@
import { ReviewAction } from '@osf/features/moderation/models';
-import { Preprint, PreprintShortInfo } from '@osf/features/preprints/models';
+import { Preprint, PreprintRequestAction, PreprintShortInfo } from '@osf/features/preprints/models';
import { PreprintRequest } from '@osf/features/preprints/models/preprint-request.models';
import { AsyncStateModel, AsyncStateWithTotalCount, OsfFile, OsfFileVersion } from '@shared/models';
@@ -11,6 +11,7 @@ export interface PreprintStateModel {
preprintVersionIds: AsyncStateModel;
preprintReviewActions: AsyncStateModel;
preprintRequests: AsyncStateModel;
+ preprintRequestsActions: AsyncStateModel;
}
export const DefaultState: PreprintStateModel = {
@@ -52,4 +53,9 @@ export const DefaultState: PreprintStateModel = {
isLoading: false,
error: null,
},
+ preprintRequestsActions: {
+ data: [],
+ isLoading: false,
+ error: null,
+ },
};
diff --git a/src/app/features/preprints/store/preprint/preprint.selectors.ts b/src/app/features/preprints/store/preprint/preprint.selectors.ts
index e5abe6d97..fa413fd7a 100644
--- a/src/app/features/preprints/store/preprint/preprint.selectors.ts
+++ b/src/app/features/preprints/store/preprint/preprint.selectors.ts
@@ -83,4 +83,14 @@ export class PreprintSelectors {
static arePreprintRequestsLoading(state: PreprintStateModel) {
return state.preprintRequests.isLoading;
}
+
+ @Selector([PreprintState])
+ static getPreprintRequestActions(state: PreprintStateModel) {
+ return state.preprintRequestsActions.data;
+ }
+
+ @Selector([PreprintState])
+ static arePreprintRequestActionsLoading(state: PreprintStateModel) {
+ return state.preprintRequestsActions.isLoading;
+ }
}
diff --git a/src/app/features/preprints/store/preprint/preprint.state.ts b/src/app/features/preprints/store/preprint/preprint.state.ts
index eb010e579..f68e61b83 100644
--- a/src/app/features/preprints/store/preprint/preprint.state.ts
+++ b/src/app/features/preprints/store/preprint/preprint.state.ts
@@ -1,5 +1,5 @@
import { Action, State, StateContext, Store } from '@ngxs/store';
-import { patch } from '@ngxs/store/operators';
+import { append, patch } from '@ngxs/store/operators';
import { tap } from 'rxjs';
import { catchError } from 'rxjs/operators';
@@ -15,10 +15,13 @@ import {
FetchPreprintById,
FetchPreprintFile,
FetchPreprintFileVersions,
+ FetchPreprintRequestActions,
FetchPreprintRequests,
FetchPreprintReviewActions,
FetchPreprintVersionIds,
ResetState,
+ SubmitRequestsDecision,
+ SubmitReviewsDecision,
WithdrawPreprint,
} from './preprint.actions';
import { DefaultState, PreprintStateModel } from './preprint.model';
@@ -60,6 +63,9 @@ export class PreprintState {
preprint: patch({ isLoading: true, data: null }),
preprintFile: patch({ isLoading: true, data: null }),
fileVersions: patch({ isLoading: true, data: [] }),
+ preprintReviewActions: patch({ isLoading: false, data: [] }),
+ preprintRequests: patch({ isLoading: false, data: [] }),
+ preprintRequestsActions: patch({ isLoading: false, data: [] }),
})
);
@@ -164,6 +170,25 @@ export class PreprintState {
);
}
+ @Action(FetchPreprintRequestActions)
+ fetchPreprintRequestsActions(ctx: StateContext, action: FetchPreprintRequestActions) {
+ ctx.setState(patch({ preprintRequestsActions: patch({ isLoading: true }) }));
+
+ return this.preprintsService.getPreprintRequestActions(action.requestId).pipe(
+ tap((actions) => {
+ ctx.setState(
+ patch({
+ preprintRequestsActions: patch({
+ isLoading: false,
+ data: append(actions),
+ }),
+ })
+ );
+ }),
+ catchError((error) => handleSectionError(ctx, 'preprintRequestsActions', error))
+ );
+ }
+
@Action(WithdrawPreprint)
withdrawPreprint(ctx: StateContext, action: WithdrawPreprint) {
const preprintId = ctx.getState().preprint.data?.id;
@@ -172,6 +197,22 @@ export class PreprintState {
return this.preprintsService.withdrawPreprint(preprintId, action.justification);
}
+ @Action(SubmitReviewsDecision)
+ submitReviewsDecision(ctx: StateContext, action: SubmitReviewsDecision) {
+ const preprintId = ctx.getState().preprint.data?.id;
+ if (!preprintId) return;
+
+ return this.preprintsService.submitReviewsDecision(preprintId, action.trigger, action.comment);
+ }
+
+ @Action(SubmitRequestsDecision)
+ submitRequestsDecision(ctx: StateContext, action: SubmitRequestsDecision) {
+ const preprintId = ctx.getState().preprint.data?.id;
+ if (!preprintId) return;
+
+ return this.preprintsService.submitRequestsDecision(action.requestId, action.trigger, action.comment);
+ }
+
@Action(ResetState)
resetState(ctx: StateContext) {
ctx.setState(patch({ ...DefaultState }));
diff --git a/src/app/shared/mappers/user/user.mapper.ts b/src/app/shared/mappers/user/user.mapper.ts
index 153af5533..e6ee2550e 100644
--- a/src/app/shared/mappers/user/user.mapper.ts
+++ b/src/app/shared/mappers/user/user.mapper.ts
@@ -34,6 +34,7 @@ export class UserMapper {
social: user.attributes.social,
defaultRegionId: user.relationships?.default_region?.data?.id,
allowIndexing: user.attributes?.allow_indexing,
+ canViewReviews: user.attributes.can_view_reviews === true, //do not simplify it
};
}
diff --git a/src/app/shared/models/user/user.models.ts b/src/app/shared/models/user/user.models.ts
index bd3f685c5..0b34071c2 100644
--- a/src/app/shared/models/user/user.models.ts
+++ b/src/app/shared/models/user/user.models.ts
@@ -17,6 +17,7 @@ export interface User {
defaultRegionId: string;
allowIndexing: boolean | undefined;
isModerator?: boolean;
+ canViewReviews: boolean;
}
export interface UserSettings {
@@ -39,6 +40,7 @@ export interface UserGetResponse {
social: Social;
date_registered: string;
allow_indexing?: boolean;
+ can_view_reviews: boolean;
};
relationships: {
default_region: {
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index eca9b949d..6e1be8e00 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -1189,7 +1189,9 @@
"public": "Public",
"embargo": "Embargo",
"pendingUpdates": "Pending Updates",
- "pendingWithdrawal": "Pending Withdrawal"
+ "pendingWithdrawal": "Pending Withdrawal",
+ "approved": "Approved",
+ "declined": "Declined"
},
"makeDecision": {
"header": "Make decision",
@@ -2039,6 +2041,10 @@
}
},
"details": {
+ "reasonForWithdrawal": "Reason for withdrawal",
+ "originalPublicationDate": "Original Publication Date",
+ "publicationDoi": "Peer-reviewed Publication DOI",
+ "supplementalMaterials": "Supplemental Materials",
"doi": {
"title": "{{documentType}} DOI",
"pendingDoiMinted": "DOIs are minted by a third party, and may take up to 24 hours to be registered.",
@@ -2065,6 +2071,7 @@
"rejected": "Rejected",
"pendingWithdrawal": "Pending withdrawal",
"withdrawalRejected": "Withdrawal rejected",
+ "withdrawn": "Withdrawn",
"messages": {
"base": "{{name}} uses {{workflow}}. This {{documentType}}",
"pendingPreModeration": "is not publicly available or searchable until approved by a moderator.",
@@ -2088,6 +2095,77 @@
"preModerationNoticeAccepted": "{{pluralCapitalizedPreprintWord}} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {{singularPreprintWord}} version removal and at the discretion of the moderators.
This service uses pre-moderation. This request will be submitted to service moderators for review. If the request is approved, this {{singularPreprintWord}} version will be replaced by a tombstone page with metadata and the reason for withdrawal. This {{singularPreprintWord}} version will still be searchable by other users after removal.",
"postModerationNotice": "{{pluralCapitalizedPreprintWord}} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {{singularPreprintWord}} version removal and at the discretion of the moderators.
This service uses post-moderation. This request will be submitted to service moderators for review. If the request is approved, this {{singularPreprintWord}} version will be replaced by a tombstone page with metadata and the reason for withdrawal. This {{singularPreprintWord}} version will still be searchable by other users after removal.",
"noModerationNotice": "{{pluralCapitalizedPreprintWord}} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {{singularPreprintWord}} version removal and at the discretion of the moderators.
This request will be submitted to {{supportEmail}} for review and removal. If the request is approved, this {{singularPreprintWord}} version will be replaced by a tombstone page with metadata and the reason for withdrawal. This {{singularPreprintWord}} version will still be searchable by other users after removal."
+ },
+ "moderationStatusBanner": {
+ "recentActivity": {
+ "pending": "submitted this {{documentType}} on",
+ "accepted": "accepted this {{documentType}} on",
+ "rejected": "rejected this {{documentType}} on",
+ "pendingWithdrawal": "requested to withdraw this {{documentType}} on",
+ "withdrawn": "withdrew this {{documentType}} on",
+ "automatic": {
+ "pending": "This {{documentType}} was submitted on",
+ "accepted": "This {{documentType}} was automatically accepted on"
+ }
+ }
+ },
+ "decision": {
+ "noReasonProvided": "No reason provided",
+ "makeDecision": "Make decision",
+ "modifyDecision": "Modify decision",
+ "withdrawalReason": "View reason",
+ "header": {
+ "submitDecision": "Submit your decision",
+ "modifyDecision": "Modify your decision",
+ "withdrawalReason": "Reason for withdrawal"
+ },
+ "settings": {
+ "comments": {
+ "private": "Comments are not visible to contributors",
+ "public": "Comments are visible to contributors on decision"
+ },
+ "names": {
+ "anonymous": "Comments are anonymous",
+ "named": "Commenter's name is visible to contributors"
+ },
+ "moderation": {
+ "pre": "Submission appears in search results once accepted",
+ "post": "Submission will be removed from search results and made private if rejected"
+ }
+ },
+ "submitButton": {
+ "submitDecision": "Submit decision",
+ "modifyDecision": "Modify decision",
+ "updateComment": "Update comment"
+ },
+ "accept": {
+ "label": "Accept submission",
+ "pre": "Submission will appear in search results and be made public.",
+ "post": "Submission will continue to appear in search results."
+ },
+ "reject": {
+ "label": "Reject submission",
+ "pre": "Submission will not appear in search results and will remain private.",
+ "post": "Submission will be removed from search results and made private."
+ },
+ "approve": {
+ "label": "Approve withdrawal",
+ "explanation": "Submission will be withdrawn but still have a tombstone page with a subset of the metadata available"
+ },
+ "decline": {
+ "label": "Decline withdrawal",
+ "explanation": "Submission will remain fully public"
+ },
+ "withdrawn": {
+ "label": "Withdraw submission",
+ "post": "Submission will no longer be publicly available."
+ },
+ "commentPlaceholder": "Explain the reasoning behind your decision (optional)",
+ "commentLengthError": "Comment is {{difference}} character(s) too long (maximum is {{limit}}).",
+ "withdrawalJustification": "Reason for withdrawal (optional, will be publicly displayed)",
+ "denialJustification": "Reason for denial (required, not publicly visible)",
+ "justificationRequiredError": "Request decision justification can't be blank",
+ "justificationLengthError": "Request decision justification is too short (minimum is {{minLength}} characters)"
}
},
"preprintContributorsWarning": "Warning: Changing your permissions will prevent you from editing your draft.",