diff --git a/app/models/preprint.ts b/app/models/preprint.ts index dd67a495f07..812359beb0b 100644 --- a/app/models/preprint.ts +++ b/app/models/preprint.ts @@ -36,6 +36,12 @@ export enum PreprintPreregLinkInfoEnum { PREREG_BOTH = 'prereg_both', } +export const VersionStatusSimpleLabelKey = { + [ReviewsState.PENDING]: 'preprints.detail.version_status.pending', + [ReviewsState.REJECTED]: 'preprints.detail.version_status.rejected', + [ReviewsState.WITHDRAWN]: 'preprints.detail.version_status.withdrawn', +}; + export interface PreprintLicenseRecordModel { copyright_holders: string[]; year: string; @@ -70,6 +76,8 @@ export default class PreprintModel extends AbstractNodeModel { @attr('string') whyNoData!: string | null; @attr('string') whyNoPrereg!: string | null; @attr('string') preregLinkInfo!: PreprintPreregLinkInfoEnum; + @attr('number') version!: number; + @attr('boolean') isLatestVersion!: boolean; @belongsTo('node', { inverse: 'preprints' }) node!: AsyncBelongsTo & NodeModel; @@ -107,6 +115,9 @@ export default class PreprintModel extends AbstractNodeModel { @hasMany('identifiers') identifiers!: AsyncHasMany; + @hasMany('preprint', { inverse: null }) + versions!: AsyncHasMany; + @alias('links.doi') articleDoiUrl!: string | null; @alias('links.preprint_doi') preprintDoiUrl!: string; @@ -123,6 +134,14 @@ export default class PreprintModel extends AbstractNodeModel { .replace(/({{year}})/g, year) .replace(/({{copyrightHolders}})/g, copyright_holders.join(', ')); } + + get currentUserIsAdmin(): boolean { + return this.currentUserPermissions.includes(Permission.Admin); + } + + get canCreateNewVersion(): boolean { + return this.currentUserIsAdmin && this.datePublished && this.isLatestVersion; + } } declare module 'ember-data/types/registries/model' { diff --git a/app/preprints/-components/preprint-doi/component-test.ts b/app/preprints/-components/preprint-doi/component-test.ts new file mode 100644 index 00000000000..f22f44463c6 --- /dev/null +++ b/app/preprints/-components/preprint-doi/component-test.ts @@ -0,0 +1,135 @@ +import { click } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { ModelInstance } from 'ember-cli-mirage'; + +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import { ReviewsState } from 'ember-osf-web/models/provider'; +import { OsfLinkRouterStub } from 'ember-osf-web/tests/integration/helpers/osf-link-router-stub'; + +module('Integration | Component | preprint-doi', function(hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + test('it renders', async function(assert) { + this.owner.unregister('service:router'); + this.owner.register('service:router', OsfLinkRouterStub); + this.store = this.owner.lookup('service:store'); + server.loadFixtures('preprint-providers'); + const mirageProvider = server.schema.preprintProviders.find('osf') as ModelInstance; + const miragePreprint = server.create('preprint', { + id: 'doied', + provider: mirageProvider, + }, 'withVersions'); + // Version 1 has a DOI and has a preprintDoiCreated date + const version1 = server.schema.preprints.find('doied_v1') as ModelInstance; + version1.update({ preprintDoiCreated: new Date('2020-02-02') }); + // Version 2 has a DOI but no preprintDoiCreated date + const version2 = server.schema.preprints.find('doied_v2') as ModelInstance; + version2.update({ preprintDoiCreated: null }); + // Version 3 is pending moderator approval and is not published, therefore has no DOI + const version3 = server.schema.preprints.find('doied_v3') as ModelInstance; + version3.update({ + preprintDoiCreated: null, + isPublished: false, + isPreprintDoi: false, // Mirage flag used to determine if a DOI should be created + }); + + const preprint = await this.store.findRecord('preprint', miragePreprint.id); + const versions = await preprint.queryHasMany('versions'); + + const provider = await this.store.findRecord('preprint-provider', mirageProvider.id); + this.set('versions', versions); + this.set('currentVersion', versions[1]); + this.set('provider', provider); + + await render(hbs` + + `); + + // check headings exist + assert.dom('[data-test-preprint-doi-heading]').exists('Preprint DOI heading exists'); + assert.dom('[data-test-preprint-doi-heading]').hasText('Preprint DOI', 'Preprint DOI heading has correct text'); + + // check dropdown exists + assert.dom('[data-test-version-select-dropdown]').exists('Version select dropdown exists'); + assert.dom('[data-test-version-select-dropdown]') + .hasText('Version 2 (Rejected)', 'Dropdown has passed in currentVersiom selected by default'); + assert.dom('[data-test-preprint-version="2"]').exists('Version 2 is shown'); + + assert.dom('[data-test-view-version-link]').exists('View in OSF button exists'); + assert.dom('[data-test-view-version-link]').hasText('View version 2', 'View version link has correct text'); + + // check version2 has DOI text + assert.dom('[data-test-no-doi-text]').doesNotExist('No DOI text does not exist'); + assert.dom('[data-test-unlinked-doi-url]').exists('Preprint DOI URL exists'); + assert.dom('[data-test-unlinked-doi-description]').exists('Preprint DOI description exists'); + assert.dom('[data-test-unlinked-doi-description]') + // eslint-disable-next-line max-len + .hasText('DOIs are minted by a third party, and may take up to 24 hours to be registered.', 'Description is correct'); + + // check version3 has DOI, but no preprintDoiCreated date + await click('[data-test-version-select-dropdown]'); + await click('[data-test-preprint-version="3"]'); + assert.dom('[data-test-unlinked-doi-url]').doesNotExist('Unlinked preprint DOI URL does not exist'); + assert.dom('[data-test-no-doi-text]').exists('No DOI text exists'); + assert.dom('[data-test-no-doi-text]').hasText('DOI created after moderator approval', 'No DOI text is correct'); + + // check version1 has DOI and preprintDoiCreated date + await click('[data-test-version-select-dropdown]'); + await click('[data-test-preprint-version="1"]'); + assert.dom('[data-test-unlinked-doi-url]').doesNotExist('Unlinked preprint DOI URL does not exist'); + assert.dom('[data-test-unlinked-doi-description]').doesNotExist('Unlinked description does not exist'); + assert.dom('[data-test-linked-doi-url]').exists('Preprint DOI URL exists'); + }); + + test('it renders statuses', async function(assert) { + this.owner.unregister('service:router'); + this.owner.register('service:router', OsfLinkRouterStub); + this.store = this.owner.lookup('service:store'); + server.loadFixtures('preprint-providers'); + const mirageProvider = server.schema.preprintProviders.find('osf') as ModelInstance; + const miragePreprint = server.create('preprint', { + id: 'doied', + provider: mirageProvider, + }, 'withVersions'); + const version1 = server.schema.preprints.find('doied_v1') as ModelInstance; + version1.update({ reviewsState: ReviewsState.ACCEPTED }); + const version2 = server.schema.preprints.find('doied_v2') as ModelInstance; + version2.update({ reviewsState: ReviewsState.PENDING }); + const version3 = server.schema.preprints.find('doied_v3') as ModelInstance; + version3.update({ reviewsState: ReviewsState.WITHDRAWN }); + + const preprint = await this.store.findRecord('preprint', miragePreprint.id); + const versions = await preprint.queryHasMany('versions'); + + const provider = await this.store.findRecord('preprint-provider', mirageProvider.id); + this.set('currentVersion', versions[0]); + this.set('versions', versions); + this.set('provider', provider); + + await render(hbs` + + `); + + await click('[data-test-version-select-dropdown]'); + assert.dom('[data-test-preprint-version="1"]').exists('Version 1 exists'); + assert.dom('[data-test-preprint-version="1"]').hasText('Version 1', 'Version 1 is accepted'); + assert.dom('[data-test-preprint-version="2"]').exists('Version 2 exists'); + assert.dom('[data-test-preprint-version="2"]').hasText('Version 2 (Pending)', 'Version 2 is pending'); + assert.dom('[data-test-preprint-version="3"]').exists('Version 3 exists'); + assert.dom('[data-test-preprint-version="3"]').hasText('Version 3 (Withdrawn)', 'Version 3 is withdrawn'); + }); +}); diff --git a/app/preprints/-components/preprint-doi/component.ts b/app/preprints/-components/preprint-doi/component.ts index 6ad155176a8..88933967ec7 100644 --- a/app/preprints/-components/preprint-doi/component.ts +++ b/app/preprints/-components/preprint-doi/component.ts @@ -1,14 +1,26 @@ +import { action } from '@ember/object'; import Component from '@glimmer/component'; -import PreprintModel from 'ember-osf-web/models/preprint'; +import { tracked } from '@glimmer/tracking'; + +import PreprintModel, { VersionStatusSimpleLabelKey } from 'ember-osf-web/models/preprint'; import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; interface InputArgs { - preprint: PreprintModel; + versions: PreprintModel[]; provider: PreprintProviderModel; + currentVersion: PreprintModel; } export default class PreprintAbstract extends Component { provider = this.args.provider; + documentType = this.provider.documentType.singularCapitalized; + + @tracked selectedVersion = this.args.currentVersion; + + reviewStateLabelKeyMap = VersionStatusSimpleLabelKey; - documentType = this.provider.documentType.singular; + @action + selectVersion(version: PreprintModel) { + this.selectedVersion = version; + } } diff --git a/app/preprints/-components/preprint-doi/styles.scss b/app/preprints/-components/preprint-doi/styles.scss new file mode 100644 index 00000000000..0ed46aeb958 --- /dev/null +++ b/app/preprints/-components/preprint-doi/styles.scss @@ -0,0 +1,8 @@ +.version-dropdown { + margin-bottom: 12px; + width: 200px; +} + +.current-version { + font-weight: bold; +} diff --git a/app/preprints/-components/preprint-doi/template.hbs b/app/preprints/-components/preprint-doi/template.hbs index 55942ff37e3..2e4ad8cc6d1 100644 --- a/app/preprints/-components/preprint-doi/template.hbs +++ b/app/preprints/-components/preprint-doi/template.hbs @@ -1,23 +1,63 @@
-

{{t 'preprints.detail.preprint_doi' documentType=this.documentType}}

- {{#if @preprint.preprintDoiUrl}} - {{#if @preprint.preprintDoiCreated}} - + {{t 'preprints.detail.preprint_doi' documentType=this.documentType}} + + {{#if @versions}} + + - {{@preprint.preprintDoiUrl}} - - {{else}} -

{{@preprint.preprintDoiUrl}}

-

{{t 'preprints.detail.preprint_pending_doi_minted'}}

- {{/if}} + {{#let (get this.reviewStateLabelKeyMap version.reviewsState) as |reviewStateLabelKey|}} + {{t 'preprints.detail.version_doi_title' number=version.version}} + {{#if reviewStateLabelKey}} + {{t (get this.reviewStateLabelKeyMap version.reviewsState)}} + {{/if}} + {{/let}} + + {{else}} - {{#if (not @preprint.public)}} - {{t 'preprints.detail.preprint_pending_doi' documentType=this.documentType}} - {{else if (and this.provider.reviewsWorkflow (not this.preprint.isPublished))}} - {{t 'preprints.detail.preprint_pending_doi_moderation'}} +

{{t 'preprints.detail.no_versions'}}

+ {{/if}} + {{#if this.selectedVersion}} + + {{t 'preprints.detail.view_version' number=this.selectedVersion.version}} + + {{#if this.selectedVersion.preprintDoiUrl}} + {{#if this.selectedVersion.preprintDoiCreated}} + + {{this.selectedVersion.preprintDoiUrl}} + + {{else}} +

{{this.selectedVersion.preprintDoiUrl}}

+

{{t 'preprints.detail.preprint_pending_doi_minted'}}

+ {{/if}} + {{else}} +

+ {{#if (not this.selectedVersion.public)}} + {{t 'preprints.detail.preprint_pending_doi' documentType=this.documentType}} + {{else if (and this.provider.reviewsWorkflow (not this.preprint.isPublished))}} + {{t 'preprints.detail.preprint_pending_doi_moderation'}} + {{else}} + {{t 'preprints.detail.no_doi'}} + {{/if}} +

{{/if}} {{/if}} -
\ No newline at end of file + diff --git a/app/preprints/-components/preprint-file-render/component.ts b/app/preprints/-components/preprint-file-render/component.ts index a32b5a2bd65..70335c5f926 100644 --- a/app/preprints/-components/preprint-file-render/component.ts +++ b/app/preprints/-components/preprint-file-render/component.ts @@ -13,13 +13,13 @@ import Media from 'ember-responsive'; interface InputArgs { - preprint: PreprintModel; - provider: PreprintProviderModel; - primaryFile: FileModel; + preprint: PreprintModel; + provider: PreprintProviderModel; + primaryFile: FileModel; } export interface VersionModel extends FileVersionModel { - downloadUrl?: string; + downloadUrl?: string; } export default class PreprintFileRender extends Component { @@ -30,22 +30,19 @@ export default class PreprintFileRender extends Component { @tracked primaryFileHasVersions = false; @tracked fileVersions: VersionModel[] = []; - primaryFile = this.args.primaryFile; - provider = this.args.provider; - preprint = this.args.preprint; - constructor(owner: unknown, args: InputArgs) { super(owner, args); taskFor(this.loadPrimaryFileVersions).perform(); - this.allowCommenting = this.provider.allowCommenting && this.preprint.isPublished && this.preprint.public; + this.allowCommenting = this.args.provider.allowCommenting + && this.args.preprint.isPublished && this.args.preprint.public; } @task @waitFor private async loadPrimaryFileVersions() { - const primaryFileVersions = (await this.primaryFile.queryHasMany('versions', { + const primaryFileVersions = (await this.args.primaryFile.queryHasMany('versions', { sort: '-id', 'page[size]': 50, })).toArray(); this.serializeVersions(primaryFileVersions); @@ -53,19 +50,19 @@ export default class PreprintFileRender extends Component { } private serializeVersions(versions: FileVersionModel[]) { - const downloadUrl = this.primaryFile.links.download as string || ''; + const downloadUrl = this.args.primaryFile.links.download as string || ''; versions.map((version: VersionModel) => { const dateFormatted = encodeURIComponent(version.dateCreated.toISOString()); const displayName = version.name.replace(/(\.\w+)?$/, ext => `-${dateFormatted}${ext}`); this.fileVersions.push( - { - name: version.name, - id: version.id, - dateCreated: version.dateCreated, - downloadUrl: `${downloadUrl}?version=${version.id}&displayName=${displayName}`, - } as VersionModel, + { + name: version.name, + id: version.id, + dateCreated: version.dateCreated, + downloadUrl: `${downloadUrl}?version=${version.id}&displayName=${displayName}`, + } as VersionModel, ); return version; }); diff --git a/app/preprints/-components/preprint-file-render/template.hbs b/app/preprints/-components/preprint-file-render/template.hbs index badebe2a5c8..fe3ce57ff59 100644 --- a/app/preprints/-components/preprint-file-render/template.hbs +++ b/app/preprints/-components/preprint-file-render/template.hbs @@ -2,22 +2,22 @@
- {{this.primaryFile.name}} + {{@primaryFile.name}}
- {{t 'preprints.detail.file_renderer.version'}}: {{this.primaryFile.currentVersion}} + {{t 'preprints.detail.file_renderer.version'}}: {{@primaryFile.currentVersion}}
- {{#if this.primaryFileHasVersions}} -

{{this.reviewerComment}}

- {{#unless this.submission.provider.reviewsCommentsAnonymous}} + {{#unless @submission.provider.reviewsCommentsAnonymous}}
{{this.reviewerName}}
{{/unless}} {{if this.theme.isProvider this.theme.provider.name (t 'preprints.detail.status_banner.brand_name')}} {{t this.moderator}} diff --git a/app/preprints/-components/preprint-tombstone/template.hbs b/app/preprints/-components/preprint-tombstone/template.hbs index 64702401cc3..ad7911cc922 100644 --- a/app/preprints/-components/preprint-tombstone/template.hbs +++ b/app/preprints/-components/preprint-tombstone/template.hbs @@ -8,7 +8,11 @@
{{/if}} - + diff --git a/app/preprints/-components/submit/file/component.ts b/app/preprints/-components/submit/file/component.ts index 64ee093aaff..5505c614ee8 100644 --- a/app/preprints/-components/submit/file/component.ts +++ b/app/preprints/-components/submit/file/component.ts @@ -87,7 +87,7 @@ export default class PreprintFile extends Component{ } @action - public async addNewfile(): Promise { + public addNewfile(): void { this.file = null; this.isFileAttached = false; this.isFileUploadDisplayed = false; diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs b/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs index 4aa4efffafd..29fb4d47b8d 100644 --- a/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs @@ -150,11 +150,4 @@
{{/if}} {{/if}} --}} - {{#if @manager.isWithdrawalButtonDisplayed}} -
- -
- {{/if}}
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts index 9f8fea8fb7a..90a932b87aa 100644 --- a/app/preprints/-components/submit/preprint-state-machine/component.ts +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -14,8 +14,8 @@ import Toast from 'ember-toastr/services/toast'; import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; import { Permission } from 'ember-osf-web/models/osf-model'; import { ReviewsState } from 'ember-osf-web/models/provider'; -import { taskFor } from 'ember-concurrency-ts'; import InstitutionModel from 'ember-osf-web/models/institution'; +import { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; export enum PreprintStatusTypeEnum { titleAndAbstract = 'titleAndAbstract', @@ -34,6 +34,7 @@ interface StateMachineArgs { preprint: PreprintModel; setPageDirty: () => void; resetPageDirty: () => void; + newVersion?: boolean; } /** @@ -44,6 +45,7 @@ export default class PreprintStateMachine extends Component{ @service router!: RouterService; @service intl!: Intl; @service toast!: Toast; + titleAndAbstractValidation = false; fileValidation = false; metadataValidation = false; @@ -52,27 +54,36 @@ export default class PreprintStateMachine extends Component{ @tracked isNextButtonDisabled = true; @tracked isPreviousButtonDisabled = true; @tracked isDeleteButtonDisplayed = false; - @tracked isWithdrawalButtonDisplayed = false; provider = this.args.provider; @tracked preprint: PreprintModel; displayAuthorAssertions = false; @tracked statusFlowIndex = 1; @tracked isEditFlow = false; + @tracked displayFileUploadStep = true; + @tracked isNewVersionFlow = this.args.newVersion; affiliatedInstitutions = [] as InstitutionModel[]; constructor(owner: unknown, args: StateMachineArgs) { super(owner, args); + if (this.args.newVersion) { + this.preprint = this.args.preprint; + return; + } if (this.args.preprint) { this.preprint = this.args.preprint; this.setValidationForEditFlow(); this.isEditFlow = true; + if (this.args.preprint.reviewsState === ReviewsState.REJECTED) { + this.displayFileUploadStep = true; + } else { + this.displayFileUploadStep = false; + } this.isDeleteButtonDisplayed = false; - taskFor(this.canDisplayWitdrawalButton).perform(); } else { this.isDeleteButtonDisplayed = true; - this.isWithdrawalButtonDisplayed = false; + this.displayFileUploadStep = true; this.preprint = this.store.createRecord('preprint', { provider: this.provider, }); @@ -81,31 +92,6 @@ export default class PreprintStateMachine extends Component{ this.displayAuthorAssertions = this.provider.assertionsEnabled; } - @task - @waitFor - private async canDisplayWitdrawalButton(): Promise { - let isWithdrawalRejected = false; - - const withdrawalRequests = await this.preprint.requests; - const withdrawalRequest = withdrawalRequests.firstObject; - if (withdrawalRequest) { - const requestActions = await withdrawalRequest.queryHasMany('actions', { - sort: '-modified', - }); - - const latestRequestAction = requestActions.firstObject; - // @ts-ignore: ActionTrigger is never - if (latestRequestAction && latestRequestAction.actionTrigger === 'reject') { - isWithdrawalRejected = true; - } - } - - this.isWithdrawalButtonDisplayed = this.isAdmin() && - (this.preprint.reviewsState === ReviewsState.ACCEPTED || - this.preprint.reviewsState === ReviewsState.PENDING) && !isWithdrawalRejected; - - } - private setValidationForEditFlow(): void { this.titleAndAbstractValidation = true; this.fileValidation = true; @@ -134,41 +120,6 @@ export default class PreprintStateMachine extends Component{ await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); } - - /** - * Callback for the action-flow component - */ - @task - @waitFor - public async onWithdrawal(): Promise { - try { - const preprintRequest = await this.store.createRecord('preprint-request', { - comment: this.preprint.withdrawalJustification, - requestType: 'withdrawal', - target: this.preprint, - }); - - await preprintRequest.save(); - - this.toast.success( - this.intl.t('preprints.submit.action-flow.success-withdrawal', - { - singularCapitalizedPreprintWord: this.provider.documentType.singularCapitalized, - }), - ); - - await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); - } catch (e) { - const errorMessage = this.intl.t('preprints.submit.action-flow.error-withdrawal', - { - singularPreprintWord: this.provider.documentType.singular, - }); - this.toast.error(errorMessage); - captureException(e, { errorMessage }); - } - } - - /** * saveOnStep * @@ -214,11 +165,38 @@ export default class PreprintStateMachine extends Component{ public async onSubmit(): Promise { this.args.resetPageDirty(); + if (this.isNewVersionFlow) { + try { + await this.preprint.save(); + let toastMessage = this.intl.t('preprints.submit.new-version.success'); + + if (this.provider.reviewsWorkflow) { + toastMessage = this.intl.t('preprints.submit.new-version.success-review'); + const reviewAction = this.store.createRecord('review-action', { + actionTrigger: ReviewActionTrigger.Submit, + target: this.preprint, + }); + await reviewAction.save(); + } else { + this.preprint.isPublished = true; + await this.preprint.save(); + } + this.toast.success(toastMessage); + this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); + } catch (e) { + const errorTitle = this.intl.t('preprints.submit.new-version.error.title'); + const errorMessage = getApiErrorMessage(e); + captureException(e, { errorMessage }); + this.toast.error(errorMessage, errorTitle); + } + return; + } + if (this.preprint.reviewsState === ReviewsState.ACCEPTED) { await this.preprint.save(); } else if (this.provider.reviewsWorkflow) { const reviewAction = this.store.createRecord('review-action', { - actionTrigger: 'submit', + actionTrigger: ReviewActionTrigger.Submit, target: this.preprint, }); await reviewAction.save(); @@ -237,6 +215,12 @@ export default class PreprintStateMachine extends Component{ @task @waitFor public async onNext(): Promise { + if (this.isNewVersionFlow) { + // no need to save original or new version on new version flow + this.statusFlowIndex++; + return; + } + if (this.isEditFlow) { this.args.resetPageDirty(); } else { @@ -249,8 +233,12 @@ export default class PreprintStateMachine extends Component{ this.titleAndAbstractValidation ) { await this.saveOnStep(); - await this.preprint.files; - this.isNextButtonDisabled = !this.fileValidation; + if (this.displayFileUploadStep) { + await this.preprint.files; + this.isNextButtonDisabled = !this.fileValidation; + } else { + this.isNextButtonDisabled = !this.metadataValidation; + } return; } else if ( this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && @@ -499,24 +487,54 @@ export default class PreprintStateMachine extends Component{ } private getTypeIndex(type: string): number { - if (type === PreprintStatusTypeEnum.titleAndAbstract) { - return 1; - } else if (type === PreprintStatusTypeEnum.file) { - return 2; - } else if (type === PreprintStatusTypeEnum.metadata) { - return 3; - } else if (type === PreprintStatusTypeEnum.authorAssertions) { - return 4; - } else if (type === PreprintStatusTypeEnum.supplements && this.displayAuthorAssertions) { - return 5; - } else if (type === PreprintStatusTypeEnum.supplements && !this.displayAuthorAssertions) { - return 4; - } else if (type === PreprintStatusTypeEnum.review && this.displayAuthorAssertions) { - return 6; - } else if (type === PreprintStatusTypeEnum.review && !this.displayAuthorAssertions) { - return 5; + if (this.isNewVersionFlow) { + if (type === PreprintStatusTypeEnum.file) { + return 1; + } else if (type === PreprintStatusTypeEnum.review) { + return 2; + } else { + return 0; + } + } + + if (this.displayFileUploadStep) { + if (type === PreprintStatusTypeEnum.titleAndAbstract) { + return 1; + } else if (type === PreprintStatusTypeEnum.file) { + return 2; + } else if (type === PreprintStatusTypeEnum.metadata) { + return 3; + } else if (type === PreprintStatusTypeEnum.authorAssertions) { + return 4; + } else if (type === PreprintStatusTypeEnum.supplements && this.displayAuthorAssertions) { + return 5; + } else if (type === PreprintStatusTypeEnum.supplements && !this.displayAuthorAssertions) { + return 4; + } else if (type === PreprintStatusTypeEnum.review && this.displayAuthorAssertions) { + return 6; + } else if (type === PreprintStatusTypeEnum.review && !this.displayAuthorAssertions) { + return 5; + } else { + return 0; + } } else { - return 0; + if (type === PreprintStatusTypeEnum.titleAndAbstract) { + return 1; + } else if (type === PreprintStatusTypeEnum.metadata) { + return 2; + } else if (type === PreprintStatusTypeEnum.authorAssertions) { + return 3; + } else if (type === PreprintStatusTypeEnum.supplements && this.displayAuthorAssertions) { + return 4; + } else if (type === PreprintStatusTypeEnum.supplements && !this.displayAuthorAssertions) { + return 3; + } else if (type === PreprintStatusTypeEnum.review && this.displayAuthorAssertions) { + return 5; + } else if (type === PreprintStatusTypeEnum.review && !this.displayAuthorAssertions) { + return 4; + } else { + return 0; + } } } @@ -572,7 +590,22 @@ export default class PreprintStateMachine extends Component{ * @returns boolean */ public shouldDisplayStatusType(type: string): boolean{ - return type === PreprintStatusTypeEnum.authorAssertions ? this.displayAuthorAssertions : true; + if (this.isNewVersionFlow) { + if (type === PreprintStatusTypeEnum.file) { + return true; + } else if (type === PreprintStatusTypeEnum.review) { + return true; + } else { + return false; + } + } + + if (type === PreprintStatusTypeEnum.file) { + return this.displayFileUploadStep; + } else if (type === PreprintStatusTypeEnum.authorAssertions) { + return this.displayAuthorAssertions; + } + return true; } /** @@ -644,13 +677,14 @@ export default class PreprintStateMachine extends Component{ @task @waitFor public async addProjectFile(file: FileModel): Promise{ - await file.copy(this.preprint, '/', 'osfstorage', { + const target = this.preprint; + await file.copy(target, '/', 'osfstorage', { conflict: 'replace', }); - const theFiles = await this.preprint.files; + const theFiles = await target.files; const rootFolder = await theFiles.firstObject!.rootFolder; const primaryFile = await rootFolder!.files; - this.preprint.set('primaryFile', primaryFile.lastObject); + target.set('primaryFile', primaryFile.lastObject); } @action @@ -674,7 +708,7 @@ export default class PreprintStateMachine extends Component{ } public isAdmin(): boolean { - return this.preprint.currentUserPermissions.includes(Permission.Admin); + return this.preprint.currentUserPermissions?.includes(Permission.Admin); } public isElementDisabled(): boolean { diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs index 8ad088e1e29..6946f7b2222 100644 --- a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs @@ -1,5 +1,7 @@ {{#if this.shouldDisplayStatusType}} -
diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss b/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss index ed405d15eec..19f0d31087a 100644 --- a/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss @@ -20,6 +20,10 @@ &.long { height: 175px; } + + &.short { + height: 40px; + } } &.mobile { @@ -32,6 +36,10 @@ &.long { height: fit-content; } + + &.short { + height: fit-content; + } } } } diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs b/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs index 0633acae74f..7fc42d26f7a 100644 --- a/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs @@ -1,5 +1,5 @@
-
+
- {{t @header + {{t @header documentType = @provider.documentType.singularCapitalized }}
diff --git a/app/preprints/-components/withdrawal-preprint/component-test.ts b/app/preprints/-components/withdrawal-preprint/component-test.ts new file mode 100644 index 00000000000..d89840419a8 --- /dev/null +++ b/app/preprints/-components/withdrawal-preprint/component-test.ts @@ -0,0 +1,102 @@ +import { click, fillIn, render, settled} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import {TestContext} from 'ember-intl/test-support'; +import { module, test } from 'qunit'; +import { setupIntl } from 'ember-intl/test-support'; +import { PreprintProviderReviewsWorkFlow, ReviewsState } from 'ember-osf-web/models/provider'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import { ModelInstance } from 'ember-cli-mirage'; + +interface ComopenntTestContext extends TestContext { + preprint: PreprintModel; + provider: PreprintProviderModel; + onWithdrawal: () => void; +} + +module('Integration | Preprint | Component | withdrawal-preprint', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + test('it renders and shows appropriate text', async function(this: ComopenntTestContext, assert) { + this.store = this.owner.lookup('service:store'); + this.intl = this.owner.lookup('service:intl'); + + server.loadFixtures('preprint-providers'); + const mirageProvider = server.schema.preprintProviders.find('osf') as ModelInstance; + const provider = await this.store.findRecord('preprint-provider', mirageProvider.id); + + server.create('preprint', { + id: 'test', + reviewsState: ReviewsState.PENDING, + provider: mirageProvider, + }); + const preprint = await this.store.findRecord('preprint', 'test'); + const onWithdrawal = () => {/* noop */}; + this.setProperties({ + preprint, + provider, + onWithdrawal, + }); + await render(hbs` + +`); + assert.dom('[data-test-withdrawal-button]').exists('Withdrawal button exists'); + assert.dom('[data-test-withdrawal-button]').hasText('Withdraw', 'Withdrawal button text is correct'); + await click('[data-test-withdrawal-button]'); + assert.dom('[data-test-dialog-heading').hasText('Withdraw Preprint', 'Withdrawal modal title is correct'); + assert.dom('[data-test-dialog-body]').containsText('You are about to withdraw this version of your preprint', + 'Withdrawal modal text contains versioning language'); + assert.dom('[data-test-dialog-body]').containsText( + 'Since this version is still pending approval and private, it can be withdrawn immediately', + 'Withdrawal modal text contains pending language for pre-moderation providers', + ); + this.preprint.reviewsState = ReviewsState.ACCEPTED; + await settled(); + + assert.dom('[data-test-dialog-body]').containsText('Preprints are a permanent part of the scholarly record', + 'Withdrawal modal text contains language for published preprints'); + assert.dom('[data-test-dialog-body]').containsText( + 'This service uses pre-moderation. This request will be submitted to service moderators for review.', + 'Withdrawal modal text contains moderation language for pre-moderation providers', + ); + this.provider.reviewsWorkflow = PreprintProviderReviewsWorkFlow.POST_MODERATION; + await settled(); + + assert.dom('[data-test-dialog-body]').containsText( + 'This service uses post-moderation. This request will be submitted to service moderators for review.', + 'Withdrawal modal text contains moderation language for post-moderation providers', + ); + this.provider.reviewsWorkflow = null; + await settled(); + + assert.dom('[data-test-dialog-body]').containsText( + 'This request will be submitted to support@osf.io for review and removal.', + 'Withdrawal modal text contains language for providers without moderation', + ); + + assert.dom('[data-test-comment-label]').hasText('Reason for withdrawal (required): *', + 'Comment input label is correct'); + assert.dom('[data-test-comment-input] textarea').exists('Comment input exists'); + assert.dom('[data-test-comment-input] textarea').hasAttribute('placeholder', 'Comment', + 'Comment input placeholder is correct'); + + assert.dom('[data-test-confirm-withdraw-button]') + .isDisabled('Withdrawal button is disabled when form is empty'); + + await fillIn('[data-test-comment-input] textarea', 'Short comment'); + assert.dom('[data-test-confirm-withdraw-button]') + .isDisabled('Withdrawal button is disabled when comment is too short'); + + await fillIn('[data-test-comment-input] textarea', 'Longer test comment that should be long enough'); + assert.dom('[data-test-confirm-withdraw-button]') + .isEnabled('Withdrawal button is enabled when comment is long enough'); + }); +}); diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts b/app/preprints/-components/withdrawal-preprint/component.ts similarity index 66% rename from app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts rename to app/preprints/-components/withdrawal-preprint/component.ts index 8bf0b036b0f..74a9b3ba8db 100644 --- a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts +++ b/app/preprints/-components/withdrawal-preprint/component.ts @@ -5,19 +5,22 @@ import buildChangeset from 'ember-osf-web/utils/build-changeset'; import { inject as service } from '@ember/service'; import Intl from 'ember-intl/services/intl'; import { waitFor } from '@ember/test-waiters'; -import { task } from 'ember-concurrency'; +import { Task, task } from 'ember-concurrency'; import { taskFor } from 'ember-concurrency-ts'; -import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import config from 'ember-osf-web/config/environment'; import { PreprintProviderReviewsWorkFlow, ReviewsState } from 'ember-osf-web/models/provider'; import { SafeString } from '@ember/template/-private/handlebars'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; const { support: { supportEmail } } = config; interface WithdrawalModalArgs { - manager: PreprintStateMachine; + preprint: PreprintModel; + provider: PreprintProviderModel; + onWithdrawal: Task; } interface WithdrawalFormFields { @@ -40,7 +43,7 @@ export default class WithdrawalComponent extends Component }), }; - withdrawalFormChangeset = buildChangeset(this.args.manager.preprint, this.withdrawalFormValidations); + withdrawalFormChangeset = buildChangeset(this.args.preprint, this.withdrawalFormValidations); /** * Calls the state machine delete method @@ -53,7 +56,7 @@ export default class WithdrawalComponent extends Component return Promise.reject(); } this.withdrawalFormChangeset.execute(); - return taskFor(this.args.manager.onWithdrawal).perform(); + return taskFor(this.args.onWithdrawal).perform(); } @action @@ -74,41 +77,41 @@ export default class WithdrawalComponent extends Component */ public get modalTitle(): string { return this.intl.t('preprints.submit.action-flow.withdrawal-modal-title', - { singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized}); + { singularPreprintWord: this.args.provider.documentType.singularCapitalized}); } /** * internationalize the modal explanation */ public get modalExplanation(): SafeString { - if (this.args.manager.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION - && this.args.manager.preprint.reviewsState === ReviewsState.PENDING + if (this.args.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION + && this.args.preprint.reviewsState === ReviewsState.PENDING ) { return this.intl.t('preprints.submit.action-flow.pre-moderation-notice-pending', { - singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, + singularPreprintWord: this.args.provider.documentType.singular, htmlSafe: true, }) as SafeString; - } else if (this.args.manager.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION + } else if (this.args.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION ) { return this.intl.t('preprints.submit.action-flow.pre-moderation-notice-accepted', { - singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, - pluralCapitalizedPreprintWord: this.args.manager.provider.documentType.pluralCapitalized, + singularPreprintWord: this.args.provider.documentType.singular, + pluralCapitalizedPreprintWord: this.args.provider.documentType.pluralCapitalized, htmlSafe: true, }) as SafeString; - } else if (this.args.manager.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.POST_MODERATION) { + } else if (this.args.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.POST_MODERATION) { return this.intl.t('preprints.submit.action-flow.post-moderation-notice', { - singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, - pluralCapitalizedPreprintWord: this.args.manager.provider.documentType.pluralCapitalized, + singularPreprintWord: this.args.provider.documentType.singular, + pluralCapitalizedPreprintWord: this.args.provider.documentType.pluralCapitalized, htmlSafe: true, }) as SafeString; } else { return this.intl.t('preprints.submit.action-flow.no-moderation-notice', { - singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, - pluralCapitalizedPreprintWord: this.args.manager.provider.documentType.pluralCapitalized, + singularPreprintWord: this.args.provider.documentType.singular, + pluralCapitalizedPreprintWord: this.args.provider.documentType.pluralCapitalized, supportEmail, htmlSafe: true, }) as SafeString; diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss b/app/preprints/-components/withdrawal-preprint/styles.scss similarity index 56% rename from app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss rename to app/preprints/-components/withdrawal-preprint/styles.scss index 79a49a58c7e..8ee8221d60f 100644 --- a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss +++ b/app/preprints/-components/withdrawal-preprint/styles.scss @@ -1,15 +1,3 @@ -.btn { - width: 145px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-evenly; -} - -.withdrawal-button { - color: $brand-danger; -} - .explanation-container { margin-bottom: 20px; } diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs b/app/preprints/-components/withdrawal-preprint/template.hbs similarity index 68% rename from app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs rename to app/preprints/-components/withdrawal-preprint/template.hbs index 539b9e1f61f..ac8455ee3cb 100644 --- a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs +++ b/app/preprints/-components/withdrawal-preprint/template.hbs @@ -1,27 +1,13 @@ - {{#if (is-mobile)}} - - {{else}} - - {{/if}} +
- {{this.modalExplanation}} +

+ {{t 'preprints.submit.action-flow.withdrawal-explanation' singularPreprintWord=@provider.documentType.singular}} +

+

+ {{this.modalExplanation}} +

{{/let}} @@ -69,7 +60,7 @@ {{t 'general.cancel'}} + {{/if}} + {{#if this.model.canDisplayWithdrawalButton}} + + {{/if}} +
@@ -46,12 +102,18 @@ />
{{#if (or this.showStatusBanner this.isWithdrawn)}} - + {{/if}}
{{#if this.model.preprint.isWithdrawn}} {{else}} @@ -181,7 +243,11 @@
{{/if}} - + {{#if this.model.preprint.articleDoiUrl}}

{{t 'preprints.detail.article_doi'}}

diff --git a/app/preprints/new-version/controller.ts b/app/preprints/new-version/controller.ts new file mode 100644 index 00000000000..9a7101f1790 --- /dev/null +++ b/app/preprints/new-version/controller.ts @@ -0,0 +1,9 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; + +export default class PreprintNewVersionController extends Controller { + @action + noop() { + return; + } +} diff --git a/app/preprints/new-version/route.ts b/app/preprints/new-version/route.ts new file mode 100644 index 00000000000..93aef80741a --- /dev/null +++ b/app/preprints/new-version/route.ts @@ -0,0 +1,58 @@ +import Store from '@ember-data/store'; +import Route from '@ember/routing/route'; +import RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; + +import Theme from 'ember-osf-web/services/theme'; +import MetaTags, { HeadTagDef } from 'ember-osf-web/services/meta-tags'; +import PreprintModel from 'ember-osf-web/models/preprint'; + +export default class PreprintNewVersionRoute extends Route { + @service store!: Store; + @service theme!: Theme; + @service router!: RouterService; + @service metaTags!: MetaTags; + + headTags?: HeadTagDef[]; + + buildRouteInfoMetadata() { + return { + osfMetrics: { + providerId: this.theme.id, + }, + }; + } + + async model(args: any) { + try { + const provider = await this.store.findRecord('preprint-provider', args.provider_id); + this.theme.providerType = 'preprint'; + this.theme.id = args.provider_id; + + const preprint: PreprintModel = await this.store.findRecord('preprint', args.guid); + + return { + provider, + preprint, + brand: provider.brand.content, + }; + } catch (e) { + this.router.transitionTo('not-found', `preprints/${args.provider_id}`); + return null; + } + } + + afterModel(model: any) { + const {provider} = model; + if (provider && provider.assets && provider.assets.favicon) { + const headTags = [{ + type: 'link', + attrs: { + rel: 'icon', + href: provider.assets.favicon, + }, + }]; + this.set('headTags', headTags); + } + } +} diff --git a/app/preprints/new-version/template.hbs b/app/preprints/new-version/template.hbs new file mode 100644 index 00000000000..61dbe11abba --- /dev/null +++ b/app/preprints/new-version/template.hbs @@ -0,0 +1,9 @@ + diff --git a/app/router.ts b/app/router.ts index be2416a6e37..8aa10d2343e 100644 --- a/app/router.ts +++ b/app/router.ts @@ -38,6 +38,7 @@ Router.map(function() { this.route('detail', { path: '/:provider_id/:guid' }); this.route('submit', { path: '/:provider_id/submit' }); this.route('edit', { path: '/:provider_id/edit/:guid' }); + this.route('new-version', { path: '/:provider_id/new-version/:guid' }); this.route('select'); this.route('my-preprints'); }); diff --git a/app/serializers/preprint.ts b/app/serializers/preprint.ts index 03362dd4093..bda304e9e7d 100644 --- a/app/serializers/preprint.ts +++ b/app/serializers/preprint.ts @@ -1,6 +1,19 @@ +import Model from '@ember-data/model'; +import { Resource } from 'osf-api'; + import OsfSerializer from './osf-serializer'; export default class PreprintSerializer extends OsfSerializer { + normalize(modelClass: Model, resourceHash: Resource) { + const result = super.normalize(modelClass, resourceHash); + // Insert a `versions` relationship to the model + result.data.relationships!.versions = { + links: { + related: resourceHash.links!.preprint_versions!, + }, + }; + return result; + } } declare module 'ember-data/types/registries/serializer' { diff --git a/mirage/config.ts b/mirage/config.ts index bfabf667589..d2bb051b22e 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -51,7 +51,7 @@ import { import { updatePassword } from './views/user-password'; import * as userSettings from './views/user-setting'; import * as wb from './views/wb'; -import { createPreprint } from './views/preprint'; +import { createPreprint, getPreprintVersions, createPreprintVersion } from './views/preprint'; const { OSF: { apiUrl, shareBaseUrl, url: osfUrl } } = config; @@ -355,6 +355,9 @@ export default function(this: Server) { return schema.preprints.find(id); }); + this.get('/preprints/:id/versions', getPreprintVersions); + this.post('/preprints/:id/versions', createPreprintVersion); + osfNestedResource(this, 'preprint', 'contributors', { path: '/preprints/:parentID/contributors/', defaultSortKey: 'index', @@ -413,6 +416,7 @@ export default function(this: Server) { defaultSortKey: 'index', relatedModelName: 'review-action', }); + this.post('/preprints/:parentID/review_actions', createReviewAction); /** * Preprint Requests diff --git a/mirage/factories/preprint.ts b/mirage/factories/preprint.ts index 858e51b5a59..014f45f0758 100644 --- a/mirage/factories/preprint.ts +++ b/mirage/factories/preprint.ts @@ -32,6 +32,7 @@ export interface PreprintTraits { rejectedWithdrawalNoComment: Trait; reviewAction: Trait; withAffiliatedInstitutions: Trait; + withVersions: Trait; } export default Factory.extend({ @@ -77,6 +78,8 @@ export default Factory.extend({ dateWithdrawn: null, doi: null, + version: 1, + isLatestVersion: true, tags() { return faker.lorem.words(5).split(' '); @@ -239,6 +242,33 @@ export default Factory.extend({ }, }), + withVersions: trait({ + afterCreate(preprint, server) { + const baseId = preprint.id; + const versionedPreprints = [1, 2, 3].map((version: number) => { + const isLatestVersion = version === 3; + return server.create('preprint', { + title: preprint.title, + description: preprint.description, + provider: preprint.provider, + id: `${baseId}_v${version}`, + reviewsState: preprint.reviewsState, + version, + isLatestVersion, + }); + }); + preprint.update({ + // A bit of a workaround since the API will return the latest version when getting baseId + version: 3, + isLatestVersion: true, + }); + + if (preprint.provider) { + preprint.provider.preprints.models.pushObjects(versionedPreprints); + } + }, + }), + reviewAction: trait({ afterCreate(preprint, server) { const creator = server.create('user', { fullName: 'Review action Commentor' }); diff --git a/mirage/factories/review-action.ts b/mirage/factories/review-action.ts index 07310c81e18..b03aefd9895 100644 --- a/mirage/factories/review-action.ts +++ b/mirage/factories/review-action.ts @@ -3,6 +3,15 @@ import faker from 'faker'; import ReviewActionModel, { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; +export interface TargetRelationship { + id: string | number; + type: 'registrations' | 'preprints'; +} +export interface MirageReviewAction { + creatorId: string; + targetId: TargetRelationship; +} + export default Factory.extend({ auto: false, visible: true, diff --git a/mirage/scenarios/preprints.ts b/mirage/scenarios/preprints.ts index 670bd1d9d04..8a87ea69faf 100644 --- a/mirage/scenarios/preprints.ts +++ b/mirage/scenarios/preprints.ts @@ -39,7 +39,7 @@ function buildOSF( const currentUserModerator = server.create('moderator', { id: currentUser.id, user: currentUser, provider: osf }, 'asAdmin'); - const rejectedAdminPreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-rejected-admin', title: 'Preprint RWF: Pre-moderation, Admin and Rejected with Review Actions comment', @@ -74,7 +74,7 @@ function buildOSF( approvedAdminPreprint.update({ identifiers: [osfApprovedAdminIdentifier] }); - const notContributorPreprint = server.create('preprint', Object({ + server.create('preprint', Object({ provider: osf, id: 'osf-not-contributor', title: 'Preprint RWF: Pre-moderation, Non-Admin and Rejected', @@ -88,7 +88,7 @@ function buildOSF( isPreprintDoi: false, })); - const rejectedPreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-rejected', title: 'Preprint RWF: Pre-moderation, Non-Admin and Rejected', @@ -99,7 +99,7 @@ function buildOSF( tags: [], }, 'isContributor'); - const approvedPreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-approved', title: 'Preprint RWF: Pre-moderation, Non-Admin and Approved', @@ -120,7 +120,7 @@ function buildOSF( conflictOfInterestStatement: `${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, }, 'isContributor'); - const orphanedPreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-orphan', title: 'Preprint RWF: Pre-moderation, Non-Admin and Approved', @@ -129,7 +129,7 @@ function buildOSF( isPreprintOrphan: true, }, 'isContributor'); - const privatePreprint = server.create('preprint', Object({ + server.create('preprint', Object({ provider: osf, id: 'osf-private', title: 'Preprint RWF: Pre-moderation, Non-Admin and Approved', @@ -139,7 +139,7 @@ function buildOSF( isPreprintDoi: false, }), 'isContributor'); - const publicDoiPreprint = server.create('preprint', Object({ + server.create('preprint', Object({ provider: osf, id: 'osf-public-doi', title: 'Preprint RWF: Pre-moderation, Non-Admin and Approved', @@ -151,7 +151,7 @@ function buildOSF( isPublished: true, }), 'isContributor'); - const notPublishedPreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-not-published', title: 'Preprint RWF: Pre-moderation, Non-Admin and Approved', @@ -160,7 +160,7 @@ function buildOSF( isPublished: false, }, 'isContributor'); - const withdrawnPreprint = server.create('preprint', Object({ + server.create('preprint', Object({ provider: osf, id: 'osf-withdrawn', title: 'Preprint Non-Admin, Not Published and withdrawn - no license - no justification', @@ -171,7 +171,7 @@ function buildOSF( addLicenseName: false, }), 'isContributor', 'withdrawn' ); - const withdrawnLicensePreprint = server.create('preprint', Object({ + server.create('preprint', Object({ provider: osf, id: 'osf-withdrawn-license', title: 'Preprint Non-Admin, Not Published and withdrawn - license - justification - tombstone', @@ -183,7 +183,7 @@ function buildOSF( description: `${faker.lorem.sentence(200)}\n${faker.lorem.sentence(100)}`, }), 'isContributor', 'withdrawn'); - const pendingWithdrawalPreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-pending-withdrawal', title: 'Preprint Non-Admin, Not Published and Pending Withdrawal', @@ -192,7 +192,7 @@ function buildOSF( isPublished: false, }, 'pendingWithdrawal', 'isContributor'); - const rejectedWithdrawalPreprintNoComment = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-rejected-withdrawal-no-comment', title: 'Preprint Non-Admin, Not Published and Rejected Withdrawal - No Comment', @@ -201,7 +201,7 @@ function buildOSF( isPublished: false, }, 'rejectedWithdrawalNoComment', 'isContributor'); - const rejectedWithdrawalPreprintComment = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-rejected-withdrawal-comment', title: 'Preprint Non-Admin, Not Published and Rejected Withdrawal - Comment - Reviews Allowed', @@ -210,7 +210,7 @@ function buildOSF( isPublished: false, }, 'rejectedWithdrawalComment', 'isContributor'); - const acceptedWithdrawalPreprintComment = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'osf-accepted-withdrawal-comment', title: 'Preprint Non-Admin, Not Published and Accepted Withdrawal - Comment - Reviews Allowed', @@ -219,7 +219,7 @@ function buildOSF( isPublished: false, }, 'acceptedWithdrawalComment'); - const examplePreprint = server.create('preprint', { + server.create('preprint', { provider: osf, id: 'khbvy', title: 'The "See Example" hard-coded preprint', @@ -234,6 +234,35 @@ function buildOSF( hasPreregLinks: PreprintPreregLinksEnum.NOT_APPLICABLE, }); + // Accepted with versions + server.create('preprint', { + provider: osf, + id: 'versioned-preprint', + title: '3 Versions Preprint', + currentUserPermissions: Object.values(Permission), + reviewsState: ReviewsState.ACCEPTED, + isPublished: true, + }, 'withVersions'); + // Withdrawn with versions + server.create('preprint', { + provider: osf, + id: 'withdrawn-preprint', + title: 'ReviewsState: Withdrawn Preprint', + currentUserPermissions: Object.values(Permission), + reviewsState: ReviewsState.WITHDRAWN, + isPublished: true, + dateWithdrawn: new Date(), + }, 'withVersions'); + // Rejected with versions + server.create('preprint', { + provider: osf, + id: 'rejected-preprint', + title: 'ReviewsState: Rejected Preprint', + currentUserPermissions: Object.values(Permission), + reviewsState: ReviewsState.REJECTED, + isPublished: true, + }, 'withVersions'); + const subjects = server.createList('subject', 7); osf.update({ @@ -247,24 +276,6 @@ function buildOSF( footer_links: '', brand, moderators: [currentUserModerator], - preprints: [ - examplePreprint, - rejectedAdminPreprint, - approvedAdminPreprint, - approvedPreprint, - rejectedPreprint, - orphanedPreprint, - privatePreprint, - notPublishedPreprint, - withdrawnPreprint, - withdrawnLicensePreprint, - pendingWithdrawalPreprint, - rejectedWithdrawalPreprintNoComment, - rejectedWithdrawalPreprintComment, - acceptedWithdrawalPreprintComment, - notContributorPreprint, - publicDoiPreprint, - ], description: 'This is the description for osf', }); } diff --git a/mirage/serializers/preprint-request.ts b/mirage/serializers/preprint-request.ts index 7ef1400186a..71210927ec4 100644 --- a/mirage/serializers/preprint-request.ts +++ b/mirage/serializers/preprint-request.ts @@ -14,14 +14,6 @@ export default class PreprintRequestSerializer extends ApplicationSerializer
) {
         const relationships: SerializedRelationships = {
-            creator: {
-                links: {
-                    related: {
-                        href: `${apiUrl}/v2/users/${model.creator.id}`,
-                        meta: {},
-                    },
-                },
-            },
             target: {
                 links: {
                     related: {
@@ -39,6 +31,16 @@ export default class PreprintRequestSerializer extends ApplicationSerializer
;
 
@@ -81,3 +85,29 @@ export function updatePreprint(this: HandlerContext, schema: Schema, request: Re
     resource.update(attributes);
     return this.serialize(resource);
 }
+
+export function getPreprintVersions(this: HandlerContext, schema: Schema) {
+    const preprintId = this.request.params.id as string;
+    const baseId = preprintId.split('_v')[0]; // assumes preprint id is of the form _v
+    const preprints = schema.preprints.all().models
+        .filter((preprint: ModelInstance) => preprint.id !== baseId && preprint.id.includes(baseId));
+    const versions = preprints.sortBy('versionNumber').reverse();
+    return process(schema, this.request, this,
+        versions.map((version: ModelInstance) => this.serialize(version).data));
+}
+
+export function createPreprintVersion(this: HandlerContext, schema: Schema) {
+    const basePreprintId = this.request.params.id as string;
+    const basePreprint = schema.preprints.find(basePreprintId);
+    basePreprint.update({ isLatestVersion: false });
+    const baseVersionNumber = basePreprint.version || 1;
+    const providerModeration = basePreprint.provider && basePreprint.provider.reviewsWorkflow;
+    const newVersion = schema.preprints.create({
+        ...basePreprint.attrs,
+        reviewsState: providerModeration ? ReviewsState.PENDING : ReviewsState.ACCEPTED,
+        id: basePreprintId.split('_v')[0] + '_v' + (baseVersionNumber + 1),
+        versionNumber: baseVersionNumber + 1,
+        isLatestVersion: true,
+    });
+    return this.serialize(newVersion);
+}
diff --git a/mirage/views/review-action.ts b/mirage/views/review-action.ts
index 37407f46a99..8e34488c86c 100644
--- a/mirage/views/review-action.ts
+++ b/mirage/views/review-action.ts
@@ -1,39 +1,61 @@
-import { HandlerContext, NormalizedRequestAttrs, Request, Schema } from 'ember-cli-mirage';
+import { HandlerContext, ModelInstance, NormalizedRequestAttrs, Request, Schema } from 'ember-cli-mirage';
+import { PreprintMirageModel } from 'ember-osf-web/mirage/factories/preprint';
+import { MirageRegistration } from 'ember-osf-web/mirage/factories/registration';
+import { MirageReviewAction } from 'ember-osf-web/mirage/factories/review-action';
+import { ReviewsState } from 'ember-osf-web/models/provider';
 import { RegistrationReviewStates } from 'ember-osf-web/models/registration';
-import ReviewActionModel, { ReviewActionTrigger } from 'ember-osf-web/models/review-action';
+import { ReviewActionTrigger } from 'ember-osf-web/models/review-action';
 import { RevisionReviewStates } from 'ember-osf-web/models/schema-response';
 
 export function createReviewAction(this: HandlerContext, schema: Schema, request: Request) {
-    const attrs = this.normalizedRequestAttrs('review-action') as Partial>;
-    const registrationId = request.params.parentID;
+    const attrs = this.normalizedRequestAttrs('review-action') as Partial>;
+    const targetId = request.params.parentID;
     const userId = schema.roots.first().currentUserId;
     let reviewAction;
-    if (userId && registrationId) {
+    if (userId && targetId) {
         const currentUser = schema.users.find(userId);
-        const registration = schema.registrations.find(registrationId);
+        const target = schema[attrs.targetId!.type].find(targetId) as
+            ModelInstance;
         const { trigger } = attrs as any; // have to cast attrs to any because `actionTrigger` does not exist on type
         reviewAction = schema.reviewActions.create({
             creator: currentUser,
-            target: registration,
+            target,
             dateCreated: new Date(),
             dateModified: new Date(),
             ...attrs,
         });
-        switch (trigger) {
-        case ReviewActionTrigger.AcceptSubmission:
-        case ReviewActionTrigger.RejectWithdrawal:
-            registration.reviewsState = RegistrationReviewStates.Accepted;
-            registration.revisionState = RevisionReviewStates.Approved;
-            break;
-        case ReviewActionTrigger.RejectSubmission:
-            registration.reviewsState = RegistrationReviewStates.Rejected;
-            break;
-        case ReviewActionTrigger.ForceWithdraw:
-        case ReviewActionTrigger.AcceptWithdrawal:
-            registration.reviewsState = RegistrationReviewStates.Withdrawn;
-            break;
-        default:
-            break;
+        if (target.modelName === 'preprint') {
+            switch (trigger) {
+            case ReviewActionTrigger.Submit:
+                target.reviewsState = ReviewsState.PENDING;
+                break;
+            case ReviewActionTrigger.RejectSubmission:
+                target.reviewsState = ReviewsState.REJECTED;
+                break;
+            case ReviewActionTrigger.ForceWithdraw:
+            case ReviewActionTrigger.AcceptWithdrawal:
+                target.reviewsState = ReviewsState.WITHDRAWN;
+                break;
+            default:
+                break;
+            }
+        } else if (target.modelName === 'registration') {
+            switch (trigger) {
+            case ReviewActionTrigger.AcceptSubmission:
+            case ReviewActionTrigger.RejectWithdrawal:
+                target.reviewsState = RegistrationReviewStates.Accepted;
+                target.revisionState = RevisionReviewStates.Approved;
+                break;
+            case ReviewActionTrigger.RejectSubmission:
+                target.reviewsState = RegistrationReviewStates.Rejected;
+                break;
+            case ReviewActionTrigger.ForceWithdraw:
+            case ReviewActionTrigger.AcceptWithdrawal:
+                target.reviewsState = RegistrationReviewStates.Withdrawn;
+                break;
+            default:
+                break;
+            }
         }
     }
     return reviewAction;
diff --git a/tests/acceptance/preprints/detail-test.ts b/tests/acceptance/preprints/detail-test.ts
new file mode 100644
index 00000000000..ffae8fa816b
--- /dev/null
+++ b/tests/acceptance/preprints/detail-test.ts
@@ -0,0 +1,263 @@
+import { currentRouteName, settled } from '@ember/test-helpers';
+import { ModelInstance } from 'ember-cli-mirage';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { percySnapshot } from 'ember-percy';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+import { setupOSFApplicationTest, visit } from 'ember-osf-web/tests/helpers';
+import PreprintProviderModel from 'ember-osf-web/models/preprint-provider';
+import PreprintModel from 'ember-osf-web/models/preprint';
+import { PreprintProviderReviewsWorkFlow, ReviewsState } from 'ember-osf-web/models/provider';
+import { Permission } from 'ember-osf-web/models/osf-model';
+import { click } from 'ember-osf-web/tests/helpers';
+
+interface PreprintDetailTestContext extends TestContext {
+    provider: ModelInstance;
+    preprint: ModelInstance;
+}
+
+module('Acceptance | preprints | detail', hooks => {
+    setupOSFApplicationTest(hooks);
+    setupMirage(hooks);
+
+    hooks.beforeEach(async function(this: PreprintDetailTestContext) {
+        server.loadFixtures('preprint-providers');
+        server.loadFixtures('citation-styles');
+        const provider = server.schema.preprintProviders.find('osf') as ModelInstance;
+        provider.update({
+            reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION,
+            assertionsEnabled: true,
+        });
+
+        const preprint = server.create('preprint', {
+            id: 'test',
+            provider,
+            currentUserPermissions: Object.values(Permission),
+            title: 'Test Preprint',
+            description: 'This is a test preprint',
+        });
+        this.provider = provider;
+        this.preprint = preprint;
+    });
+
+    test('Accepted preprint detail page', async function(this: PreprintDetailTestContext, assert) {
+        this.preprint.update({
+            reviewsState: ReviewsState.ACCEPTED,
+        });
+        await visit('/preprints/osf/test');
+        assert.equal(currentRouteName(), 'preprints.detail', 'Current route is preprint detail');
+
+        // Check page title
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        assert.equal(pageTitle, 'OSF Preprints | Test Preprint', 'Page title is correct');
+
+        // Check preprint title
+        assert.dom('[data-test-preprint-title]').exists('Title is displayed');
+        assert.dom('[data-test-preprint-title]').hasText('Test Preprint', 'Title is correct');
+
+        // Check edit and new version buttons
+        assert.dom('[data-test-edit-preprint-button]').exists('Edit button is displayed');
+        assert.dom('[data-test-edit-preprint-button]').containsText('Edit', 'Edit button text is correct');
+        assert.dom('[data-test-create-new-version-button]').exists('New version button is displayed');
+        assert.dom('[data-test-withdrawal-button]').exists('Withdraw button is displayed');
+
+        // Check preprint authors
+        assert.dom('[data-test-contributor-name]').exists('Authors are displayed');
+
+        // TODO: Check author assertions
+
+        // Check preprint status banner
+        assert.dom('[data-test-status]').exists('Status banner is displayed');
+        assert.dom('[data-test-status]').containsText('accepted', 'Status is correct');
+        await percySnapshot(assert);
+    });
+
+    test('Accepted preprint, prior version detail page', async function(this: PreprintDetailTestContext, assert) {
+        this.preprint.update({
+            reviewsState: ReviewsState.ACCEPTED,
+            isLatestVersion: false,
+        });
+        await visit('/preprints/osf/test');
+
+        // Check edit and new version buttons
+        assert.dom('[data-test-edit-preprint-button]').doesNotExist('Edit button is not displayed for prior versions');
+        assert.dom('[data-test-create-new-version-button]')
+            .doesNotExist('New version button is not displayed for prior versions');
+        assert.dom('[data-test-withdrawal-button]').exists('Withdraw button is displayed for prior versions');
+
+        // Check preprint status banner
+        assert.dom('[data-test-status]').exists('Status banner is displayed');
+        assert.dom('[data-test-status]').containsText('accepted', 'Status is correct');
+        await percySnapshot(assert);
+    });
+
+    test('Pre-mod: Rejected preprint detail page', async function(this: PreprintDetailTestContext, assert) {
+        this.provider.update({
+            reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION,
+        });
+        this.preprint.update({
+            reviewsState: ReviewsState.REJECTED,
+            datePublished: null,
+        });
+        await visit('/preprints/osf/test');
+        assert.equal(currentRouteName(), 'preprints.detail', 'Current route is preprint detail');
+
+        // Check page title. Should be same as accepted preprint
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        assert.equal(pageTitle, 'OSF Preprints | Test Preprint', 'Page title is correct');
+
+        // Check preprint title. Should be same as accepted preprint
+        assert.dom('[data-test-preprint-title]').exists('Title is displayed');
+        assert.dom('[data-test-preprint-title]').hasText('Test Preprint', 'Title is correct');
+
+        // Check edit and new version buttons
+        assert.dom('[data-test-edit-preprint-button]').exists('Edit button is displayed');
+        assert.dom('[data-test-edit-preprint-button]')
+            .hasText('Edit and resubmit', 'Edit button text indicates resubmission');
+        assert.dom('[data-test-create-new-version-button]').doesNotExist('New version button is not displayed');
+        assert.dom('[data-test-withdrawal-button]').doesNotExist('Withdraw button is not displayed');
+
+        // Check preprint authors
+        assert.dom('[data-test-contributor-name]').exists('Authors are displayed');
+
+        // Check preprint status banner
+        assert.dom('[data-test-status]').exists('Status banner is displayed');
+        assert.dom('[data-test-status]').containsText('rejected', 'Status is correct');
+        await percySnapshot(assert);
+    });
+
+
+    test('Withdrawn preprint, only version detail page', async function(this: PreprintDetailTestContext, assert) {
+        this.preprint.update({
+            dateWithdrawn: new Date(),
+        });
+        await visit('/preprints/osf/test');
+
+        // Check page title
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        assert.equal(pageTitle, 'OSF Preprints | Withdrawn: Test Preprint', 'Page title is correct');
+
+        // Check new version button and no edit button
+        assert.dom('[data-test-edit-preprint-button]').doesNotExist('Edit button is not displayed');
+        assert.dom('[data-test-create-new-version-button]')
+            .exists('New version button is displayed for latest withdrawn preprint version');
+        assert.dom('[data-test-withdrawal-button]').doesNotExist('Withdraw button is not displayed');
+        assert.dom('[data-test-previous-versions-button]').exists('Previous versions button is displayed');
+        await click('[data-test-previous-versions-button]');
+        assert.dom('[data-test-no-other-versions]').exists({ count: 1 }, 'No other versions message is displayed');
+        assert.dom('[data-test-version-link]').doesNotExist('No links to previous versions are displayed');
+        await percySnapshot(assert);
+    });
+
+    test('Withdrawn preprint, prior version detail page', async function(this: PreprintDetailTestContext, assert) {
+        this.preprint = server.create('preprint', {
+            id: 'test',
+            dateWithdrawn: new Date(),
+            currentUserPermissions: Object.values(Permission),
+            title: 'Test Preprint',
+            reviewsState: ReviewsState.WITHDRAWN,
+            description: 'This is a test preprint',
+        }, 'withVersions');
+        this.preprint.update({
+            isLatestVersion: false,
+            provider: this.provider,
+        });
+
+        await visit('/preprints/osf/test');
+
+        // Check no new version button and no edit button
+        assert.dom('[data-test-edit-preprint-button]').doesNotExist('Edit button is not displayed');
+        assert.dom('[data-test-create-new-version-button]')
+            .doesNotExist('New version button is not displayed for prior withdrawn preprint version');
+        assert.dom('[data-test-withdrawal-button]').doesNotExist('Withdraw button is not displayed');
+        assert.dom('[data-test-previous-versions-button]').exists('Previous versions button is displayed');
+        await click('[data-test-previous-versions-button]');
+        assert.dom('[data-test-version-link]').exists({ count: 3 }, 'Link to previous version is displayed');
+        assert.dom('[data-test-no-other-versions]').doesNotExist('No other versions message is not displayed');
+        await percySnapshot(assert);
+    });
+
+    test('Edit button visibility', async function(this: PreprintDetailTestContext, assert) {
+        // Read only
+        this.preprint.update({
+            currentUserPermissions: [Permission.Read],
+            reviewsState: ReviewsState.ACCEPTED,
+        });
+
+        const preprint: PreprintModel = this.owner.lookup('service:store').findRecord('preprint', 'test');
+        await visit('/preprints/osf/test');
+        assert.dom('[data-test-edit-preprint-button]').doesNotExist('Edit button is not displayed for read-only users');
+
+        // Non-latest
+        preprint.setProperties({
+            currentUserPermissions: Object.values(Permission),
+            reviewsState: ReviewsState.ACCEPTED,
+            isLatestVersion: false,
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]')
+            .doesNotExist('Edit button is not displayed for non-latest versions');
+
+        // Not initial, pre-mod, rejected
+        preprint.setProperties({
+            reviewsState: ReviewsState.REJECTED,
+            version: 4,
+            isLatestVersion: false,
+            currentUserPermissions: Object.values(Permission),
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]')
+            .doesNotExist('Edit button is not displayed for non-initial pre-mod rejected');
+
+        // Initial, pre-mod, rejected
+        preprint.setProperties({
+            reviewsState: ReviewsState.REJECTED,
+            version: 1,
+            isLatestVersion: false,
+            currentUserPermissions: Object.values(Permission),
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]').exists('Edit button is displayed for initial pre-mod rejected');
+        assert.dom('[data-test-edit-preprint-button]')
+            .containsText('Edit and resubmit', 'Edit and resubmit option for initial pre-mod rejected');
+
+        // Pre-mod, pending
+        preprint.setProperties({
+            reviewsState: ReviewsState.PENDING,
+            isLatestVersion: false,
+            currentUserPermissions: Object.values(Permission),
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]').exists('Edit button is displayed for pre-mod pending');
+        assert.dom('[data-test-edit-preprint-button]').containsText('Edit', 'Edit option for pre-mod pending');
+
+        // Withdrawn
+        preprint.setProperties({
+            dateWithdrawn: new Date(),
+            currentUserPermissions: Object.values(Permission),
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]').doesNotExist('Edit button is not displayed for withdrawn');
+
+        // Latest
+        preprint.setProperties({
+            dateWithdrawn: null,
+            isLatestVersion: true,
+            currentUserPermissions: Object.values(Permission),
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]').exists('Edit button is displayed for latest version');
+        assert.dom('[data-test-edit-preprint-button]').containsText('Edit', 'Edit option for latest version');
+
+        // Inital state
+        preprint.setProperties({
+            reviewsState: ReviewsState.INITIAL,
+            isLatestVersion: false,
+            currentUserPermissions: Object.values(Permission),
+        });
+        await settled();
+        assert.dom('[data-test-edit-preprint-button]').exists('Edit button is displayed for initial state');
+        assert.dom('[data-test-edit-preprint-button]').containsText('Edit', 'Edit option for initial state');
+    });
+});
diff --git a/tests/acceptance/preprints/edit-test.ts b/tests/acceptance/preprints/edit-test.ts
new file mode 100644
index 00000000000..43d6c654ea7
--- /dev/null
+++ b/tests/acceptance/preprints/edit-test.ts
@@ -0,0 +1,130 @@
+import { currentRouteName } from '@ember/test-helpers';
+import { ModelInstance } from 'ember-cli-mirage';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+import { setupOSFApplicationTest, visit } from 'ember-osf-web/tests/helpers';
+import PreprintProviderModel from 'ember-osf-web/models/preprint-provider';
+import PreprintModel from 'ember-osf-web/models/preprint';
+import { ReviewsState } from 'ember-osf-web/models/provider';
+import { Permission } from 'ember-osf-web/models/osf-model';
+
+interface PreprintEditTestContext extends TestContext {
+    provider: ModelInstance;
+    miragePreprint: ModelInstance;
+}
+
+module('Acceptance | preprints | edit', hooks => {
+    setupOSFApplicationTest(hooks);
+    setupMirage(hooks);
+
+    hooks.beforeEach(async function(this: PreprintEditTestContext) {
+        server.loadFixtures('preprint-providers');
+        server.create('user', 'loggedIn');
+        const provider = server.schema.preprintProviders.find('osf') as ModelInstance;
+        const miragePreprint = server.create('preprint', {
+            id: 'test',
+            provider,
+            currentUserPermissions: Object.values(Permission),
+        });
+
+        this.provider = provider;
+        this.miragePreprint = miragePreprint;
+    });
+
+    test('LeftNav for accepted preprint with author assertions', async function(this: PreprintEditTestContext, assert) {
+        this.provider.update({ assertionsEnabled: true });
+        this.miragePreprint.update({reviewsState: ReviewsState.ACCEPTED});
+        await visit('/preprints/osf/edit/test');
+        assert.equal(currentRouteName(), 'preprints.edit', 'Current route is preprint edit');
+
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        // TODO: Edit page title should have provider's preprintWord in title. Note the space after "Edit" in next line
+        assert.equal(pageTitle, 'OSF Preprints | Edit ', 'Page title is correct');
+
+        // Check leftnav for Title and Abstract, Metadata, Author Assertsions, Supplements and Review steps
+        // Should have no File step
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Supplements"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists();
+        assert.dom('[data-test-preprint-submission-step="File"]')
+            .doesNotExist('File step should not be present when editing an accepted preprint');
+    });
+
+    test('LeftNav for rejected preprint with author assertions', async function(this: PreprintEditTestContext, assert) {
+        this.provider.update({ assertionsEnabled: true });
+        this.miragePreprint.update({reviewsState: ReviewsState.REJECTED});
+
+        await visit('/preprints/osf/edit/test');
+        // Check leftnav for Title and Abstract, FILE, Metadata, Author Assertsions, Supplements and Review steps
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"]').exists();
+        assert.dom('[data-test-preprint-submission-step="File"]')
+            .exists('File upload step present when editing a rejected preprint');
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Supplements"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists();
+    });
+
+    // Same as above, but with assertions disabled
+    test('LeftNav for accepted preprint wihout assertions', async function(this: PreprintEditTestContext, assert) {
+        this.provider.update({ assertionsEnabled: false });
+        this.miragePreprint.update({reviewsState: ReviewsState.ACCEPTED});
+
+        await visit('/preprints/osf/edit/test');
+        // Check leftnav for Title and Abstract, Metadata, Supplements and Review steps
+        // Should have no File step, and no Author Assertions step
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"]').exists();
+        assert.dom('[data-test-preprint-submission-step="File"]')
+            .doesNotExist('File step should not be present when editing an accepted preprint');
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]')
+            .doesNotExist('Author Assertions step should not be present when author assertions are disabled');
+        assert.dom('[data-test-preprint-submission-step="Supplements"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists();
+    });
+
+    test('LeftNav for rejected preprint without assertions', async function(this: PreprintEditTestContext, assert) {
+        this.provider.update({ assertionsEnabled: false });
+        this.miragePreprint.update({reviewsState: ReviewsState.REJECTED});
+
+        await visit('/preprints/osf/edit/test');
+        // Check leftnav for Title and Abstract, FILE, Metadata, Supplements and Review steps
+        // Should have no Author Assertions step
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"]').exists();
+        assert.dom('[data-test-preprint-submission-step="File"]')
+            .exists('File upload step present when editing a rejected preprint');
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]')
+            .doesNotExist('Author Assertions step should not be present when author assertions are disabled');
+        assert.dom('[data-test-preprint-submission-step="Supplements"]').exists();
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists();
+    });
+
+    test('Edit workflow prepopulated with preprint info', async function(this: PreprintEditTestContext, assert) {
+        this.provider.update({ assertionsEnabled: true });
+        this.miragePreprint.update({
+            reviewsState: ReviewsState.ACCEPTED,
+            title: 'My preprint',
+            description: 'This is a my preprint',
+        });
+
+        await visit('/preprints/osf/edit/test');
+        // Check current step is Title and Abstract
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"] [data-test-icon]')
+            .hasClass('fa-dot-circle', 'Title and Abstract icon shows as active');
+
+        // check that the title and abstract are prepopulated
+        assert.dom('[data-test-title-input] input').hasValue('My preprint', 'Title input is prepopulated');
+        assert.dom('[data-test-abstract-input] textarea')
+            .hasValue('This is a my preprint', 'Abstract input is prepopulated');
+
+        // check rightnav validation status
+        assert.dom('[data-test-next-button]').exists('Next button present on Title and Abstract step');
+        assert.dom('[data-test-next-button]').isEnabled('Next button enabled on Title and Abstract step');
+        assert.dom('[data-test-submit-button]').doesNotExist('Submit button not present on Title and Abstract step');
+    });
+});
diff --git a/tests/acceptance/preprints/new-version-test.ts b/tests/acceptance/preprints/new-version-test.ts
new file mode 100644
index 00000000000..185fbe4acb2
--- /dev/null
+++ b/tests/acceptance/preprints/new-version-test.ts
@@ -0,0 +1,52 @@
+import { currentRouteName } from '@ember/test-helpers';
+import { ModelInstance } from 'ember-cli-mirage';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { percySnapshot } from 'ember-percy';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+import { setupOSFApplicationTest, visit } from 'ember-osf-web/tests/helpers';
+import PreprintProviderModel from 'ember-osf-web/models/preprint-provider';
+import PreprintModel from 'ember-osf-web/models/preprint';
+import { ReviewsState } from 'ember-osf-web/models/provider';
+import { Permission } from 'ember-osf-web/models/osf-model';
+
+interface PreprintNewVersionTestContext extends TestContext {
+    provider: ModelInstance;
+    miragePreprint: ModelInstance;
+}
+
+module('Acceptance | preprints | new version', hooks => {
+    setupOSFApplicationTest(hooks);
+    setupMirage(hooks);
+
+    hooks.beforeEach(async function(this: PreprintNewVersionTestContext) {
+        server.loadFixtures('preprint-providers');
+        server.create('user', 'loggedIn');
+        const provider = server.schema.preprintProviders.find('osf') as ModelInstance;
+        const miragePreprint = server.create('preprint', {
+            id: 'test',
+            provider,
+            currentUserPermissions: Object.values(Permission),
+        });
+
+        this.provider = provider;
+        this.miragePreprint = miragePreprint;
+    });
+
+    test('LeftNav for new preprint version', async function(this: PreprintNewVersionTestContext, assert) {
+        this.miragePreprint.update({reviewsState: ReviewsState.ACCEPTED});
+        await visit('/preprints/osf/new-version/test');
+        assert.equal(currentRouteName(), 'preprints.new-version', 'Current route is preprint new-version');
+
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        assert.equal(pageTitle, 'OSF Preprints | New Version', 'Page title is correct');
+
+        assert.dom('[data-test-preprint-submission-step="File"]').exists('File step present');
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').doesNotExist('Metadata step not present');
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]')
+            .doesNotExist('Author Assertions step not present');
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists('Review step present');
+        await percySnapshot(assert);
+    });
+});
diff --git a/tests/acceptance/preprints/submit-test.ts b/tests/acceptance/preprints/submit-test.ts
new file mode 100644
index 00000000000..cf79fe03aca
--- /dev/null
+++ b/tests/acceptance/preprints/submit-test.ts
@@ -0,0 +1,105 @@
+import { currentRouteName } from '@ember/test-helpers';
+import { ModelInstance } from 'ember-cli-mirage';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { TestContext } from 'ember-test-helpers';
+import { module, test } from 'qunit';
+
+import { click, setupOSFApplicationTest, visit } from 'ember-osf-web/tests/helpers';
+import PreprintProviderModel from 'ember-osf-web/models/preprint-provider';
+
+interface PreprintSubmitTestContext extends TestContext {
+    provider: ModelInstance;
+}
+
+module('Acceptance | preprints | submit', hooks => {
+    setupOSFApplicationTest(hooks);
+    setupMirage(hooks);
+
+    hooks.beforeEach(async function(this: PreprintSubmitTestContext) {
+        server.loadFixtures('preprint-providers');
+        server.create('user', 'loggedIn');
+    });
+
+    test('Select a provider workflow', async function(this: PreprintSubmitTestContext, assert) {
+        await visit('/preprints');
+        assert.equal(currentRouteName(), 'preprints.index', 'Current route is preprints landing page');
+
+        // Preprint provider select page
+        await click('[data-test-add-a-preprint-osf-navbar]');
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        assert.equal(pageTitle, 'OSF Preprints | Select Providers', 'Provider select page title is correct');
+        assert.dom('[data-test-create-preprints]').isDisabled('Create preprint button is disabled by default');
+        assert.dom('[data-test-provider-id="osf"]').exists('OSF provider is displayed');
+        assert.dom('[data-test-provider-id="osf"] [data-test-select-button]').exists('OSF provider has select button');
+        assert.dom('[data-test-provider-id="osf"] [data-test-select-button]')
+            .hasText('Select', 'Select button has text when provider is not selected');
+
+        // Select OSF provider
+        await click('[data-test-provider-id="osf"] [data-test-select-button]');
+        assert.dom('[data-test-create-preprints]')
+            .isEnabled('Create preprint button is enabled after selecting provider');
+        assert.dom('[data-test-provider-id="osf"] [data-test-select-button]')
+            .hasText('Deselect', 'Select button language changes after selecting provider');
+
+        // Create preprint workflow
+        await click('[data-test-create-preprints]');
+        assert.equal(currentRouteName(), 'preprints.submit', 'Current route is preprints submit page');
+    });
+
+    test('Preprint submit page with assertions', async function(this: PreprintSubmitTestContext, assert) {
+        await visit('/preprints/osf/submit');
+        assert.equal(currentRouteName(), 'preprints.submit', 'Current route is preprints submit page');
+
+        // Check leftnav items
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"]')
+            .exists('Title and Abstract step is displayed');
+        assert.dom('[data-test-preprint-submission-step="File"]').exists('File step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').exists('Metadata step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]')
+            .exists('Author Assertions step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Supplements"]').exists('Supplements step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists('Review step is displayed');
+    });
+
+    test('Preprint submit page with no assertions', async function(this: PreprintSubmitTestContext, assert) {
+        const osfProvider = server.schema.preprintProviders.find('osf') as ModelInstance;
+        osfProvider.update({ assertionsEnabled: false });
+        await visit('/preprints/osf/submit');
+        assert.equal(currentRouteName(), 'preprints.submit', 'Current route is preprints submit page');
+
+        // Check leftnav items
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"]')
+            .exists('Title and Abstract step is displayed');
+        assert.dom('[data-test-preprint-submission-step="File"]').exists('File step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Metadata"]').exists('Metadata step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Author Assertions"]')
+            .doesNotExist('Author Assertions step is NOT displayed');
+        assert.dom('[data-test-preprint-submission-step="Supplements"]').exists('Supplements step is displayed');
+        assert.dom('[data-test-preprint-submission-step="Review"]').exists('Review step is displayed');
+    });
+
+    test('Preprint submit page: Title and abstract', async function(this: PreprintSubmitTestContext, assert) {
+        await visit('/preprints/osf/submit');
+        const pageTitle = document.getElementsByTagName('title')[0].innerText;
+        // TODO: Submit page title should have provider's preprintWord in title. Note the space after "New" in next line
+        assert.equal(pageTitle, 'OSF Preprints | New ', 'Provider select page title is correct');
+
+        assert.dom('[data-test-preprint-submission-step="Title and Abstract"] [data-test-icon]')
+            .hasClass('fa-dot-circle', 'Title and Abstract step has selected icon');
+        assert.dom('[data-test-preprint-submission-step="File"] [data-test-icon]')
+            .hasClass('fa-circle', 'File step has unselected icon');
+
+        // Preprint submit page rightnav
+        assert.dom('[data-test-next-button]').exists('Next button is displayed');
+        assert.dom('[data-test-next-button]').hasText('Next', 'Next button has text');
+        assert.dom('[data-test-next-button]').isDisabled('Next button is disabled upon creating new preprint');
+        assert.dom('[data-test-submit-button]').doesNotExist('Submit button is not displayed on first step');
+        assert.dom('[data-test-delete-button]').exists('Delete button is displayed');
+
+        // Preprint submit page main content
+        assert.dom('[data-test-title-label]').exists('Title label is displayed');
+        assert.dom('[data-test-title-input]').exists('Title input is displayed');
+        assert.dom('[data-test-abstract-label]').exists('Abstract label is displayed');
+        assert.dom('[data-test-abstract-input]').exists('Abstract input is displayed');
+    });
+});
diff --git a/translations/en-us.yml b/translations/en-us.yml
index 8d6e544b5d7..929d8a464fc 100644
--- a/translations/en-us.yml
+++ b/translations/en-us.yml
@@ -1314,6 +1314,7 @@ preprints:
         edit-permission-error: 'User does not have permission to edit this {singularPreprintWord}'
         title-submit: 'New {documentType}'
         title-edit: 'Edit {documentType}'
+        title-new-version: 'New Version'
         step-title:
             title: 'Title and Abstract'
             title-input: 'Title'
@@ -1432,20 +1433,30 @@ preprints:
             error-withdrawal: 'Error withdrawing the {singularPreprintWord}.'
             next: 'Next'
             next-disabled-tooltip: 'Fill in "Required *" fields to continue'
-            no-moderation-notice: '{pluralCapitalizedPreprintWord} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {singularPreprintWord} 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} will be replaced by a tombstone page with metadata and the reason for withdrawal. This {singularPreprintWord} will still be searchable by other users after removal.' - post-moderation-notice: '{pluralCapitalizedPreprintWord} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {singularPreprintWord} 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} will be replaced by a tombstone page with metadata and the reason for withdrawal. This {singularPreprintWord} will still be searchable by other users after removal.' - pre-moderation-notice-accepted: '{pluralCapitalizedPreprintWord} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {singularPreprintWord} 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} will be replaced by a tombstone page with metadata and the reason for withdrawal. This {singularPreprintWord} will still be searchable by other users after removal.' - pre-moderation-notice-pending: 'Your {singularPreprintword} is still pending approval and thus private, but can be withdrawn immediately. If you wish to provide a reason for withdrawal, it will be displayed only to service moderators. Once withdrawn, your preprint will never be made public.' + withdrawal-explanation: 'You are about to withdraw this version of your {singularPreprintWord}. Withdrawing a version will remove it from public view but will not affect other versions of this {singularPreprintWord}, if available.' + no-moderation-notice: '{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.' + post-moderation-notice: '{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.' + pre-moderation-notice-accepted: '{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.' + pre-moderation-notice-pending: 'Since this version is still pending approval and private, it can be withdrawn immediately. The reason of withdrawal will be visible to service moderators. Once withdrawn, the {singularPreprintWord} will remain private and never be made public.' save-before-exit: 'Unsaved changes present. Are you sure you want to leave this page?' success: '{singularPreprintWord} saved.' success-withdrawal: 'Your {singularCapitalizedPreprintWord} has been successfully withdrawn.' + submit: 'Submit' withdraw-button: 'Withdraw' withdrawal-input-error: '25 characters' withdrawal-label: 'Reason for withdrawal (required):' withdrawal-modal-title: 'Withdraw {singularPreprintWord}' withdrawal-placeholder: 'Comment' + new-version: + success: 'New version created.' + success-review: 'New version submitted for moderation.' + redirect: + title: 'Cannot create a new version' + permissions: 'You do not have permission to create a new version of this {preprintWord}.' + latest-published: 'A new version can only be created from the latest published version of a {preprintWord}.' + error: + title: 'Error creating a new version' detail: abstract: 'Abstract' article_doi: 'Peer-reviewed Publication DOI' @@ -1466,13 +1477,23 @@ preprints: original_publication_date: 'Original Publication Date' orphan_preprint: 'The user has removed this file.' preprint_doi: '{documentType} DOI' + version_doi_title: 'Version {number}' + view_version: 'View version {number}' + version_status: + pending: '(Pending)' + rejected: '(Rejected)' + withdrawn: '(Withdrawn)' preprint_pending_doi: 'DOI created after {documentType} is made public' preprint_pending_doi_moderation: 'DOI created after moderator approval' + no_doi: 'No DOI' preprint_pending_doi_minted: 'DOIs are minted by a third party, and may take up to 24 hours to be registered.' private_preprint_warning: 'This {documentType} is private. Contact {supportEmail} if this is in error.' - project_button: - edit_preprint: 'Edit {documentType}' - edit_resubmit_preprint: 'Edit and resubmit' + edit_preprint: 'Edit {documentType}' + edit_resubmit_preprint: 'Edit and resubmit' + create_new_version: 'Create new version' + other_versions: 'Other versions' + preprint_version_number: 'Version {number}' + no_other_versions: 'No other versions' see_less: 'See less' see_more: 'See more' share: diff --git a/types/osf-api.d.ts b/types/osf-api.d.ts index 3b10b28592d..b0a4091aa52 100644 --- a/types/osf-api.d.ts +++ b/types/osf-api.d.ts @@ -96,5 +96,6 @@ export interface NormalLinks extends JSONAPI.Links { self?: JSONAPI.Link; html?: JSONAPI.Link; iri?: JSONAPI.Link; + preprint_versions?: JSONAPI.Link; } /* eslint-enable camelcase */