diff --git a/app/models/preprint.ts b/app/models/preprint.ts index 25d301365e..dd67a495f0 100644 --- a/app/models/preprint.ts +++ b/app/models/preprint.ts @@ -5,6 +5,8 @@ import AbstractNodeModel from 'ember-osf-web/models/abstract-node'; import CitationModel from 'ember-osf-web/models/citation'; import PreprintRequestModel from 'ember-osf-web/models/preprint-request'; import { ReviewsState } from 'ember-osf-web/models/provider'; +import ReviewActionModel from 'ember-osf-web/models/review-action'; +import InstitutionModel from 'ember-osf-web/models/institution'; import ContributorModel from './contributor'; import FileModel from './file'; @@ -81,6 +83,12 @@ export default class PreprintModel extends AbstractNodeModel { @belongsTo('preprint-provider', { inverse: 'preprints' }) provider!: AsyncBelongsTo & PreprintProviderModel; + @hasMany('institution') + affiliatedInstitutions!: AsyncHasMany; + + @hasMany('review-action') + reviewActions!: AsyncHasMany; + @hasMany('contributors', { inverse: 'preprint'}) contributors!: AsyncHasMany & ContributorModel; diff --git a/app/preprints/-components/preprint-affiliated-institutions/styles.scss b/app/preprints/-components/preprint-affiliated-institutions/styles.scss new file mode 100644 index 0000000000..9956df0467 --- /dev/null +++ b/app/preprints/-components/preprint-affiliated-institutions/styles.scss @@ -0,0 +1,50 @@ +.osf-institution-link-flex { + img { + width: 35px; + height: 35px; + } + + a { + padding-bottom: 5px; + } + + .img-circle { + border-radius: 50%; + margin-right: 15px; + } + + .img-responsive { + max-width: 100%; + } + + .img-horizontal { + margin-top: 10px; + } + + .link-horizontal { + display: inline; + } + + .link-vertical { + display: block; + } + +} + +.title { + margin-top: 10px; + font-weight: bold; + font-size: 18px; + padding-bottom: 10px; +} + +.content-container { + width: 100%; + margin-top: 20px; + + h4 { + margin-top: 10px; + margin-bottom: 10px; + font-weight: bold; + } +} diff --git a/app/preprints/-components/preprint-affiliated-institutions/template.hbs b/app/preprints/-components/preprint-affiliated-institutions/template.hbs new file mode 100644 index 0000000000..f6256c5a52 --- /dev/null +++ b/app/preprints/-components/preprint-affiliated-institutions/template.hbs @@ -0,0 +1,26 @@ +{{#if @preprint.affiliatedInstitutions}} +
+
+ {{t 'preprints.detail.affiliated_institutions'}} +
+
+ {{#each @preprint.affiliatedInstitutions as |institution|}} + + {{institution.name}} + {{#if @isReviewPage}} + + {{institution.name}} + + {{else}} + {{institution.name}} + {{/if}} + + {{/each}} +
+
+{{/if}} diff --git a/app/preprints/-components/preprint-institutions/institution-manager/component-test.ts b/app/preprints/-components/preprint-institutions/institution-manager/component-test.ts new file mode 100644 index 0000000000..77c9263ec0 --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-manager/component-test.ts @@ -0,0 +1,308 @@ +import { click, render} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupRenderingTest} from 'ember-qunit'; +import { module, test } from 'qunit'; +import { setupIntl } from 'ember-intl/test-support'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import { ModelInstance } from 'ember-cli-mirage'; +import PreprintProvider from 'ember-osf-web/models/preprint-provider'; +import InstitutionModel from 'ember-osf-web/models/institution'; +import { Permission } from 'ember-osf-web/models/osf-model'; + + +module('Integration | Preprint | Component | Institution Manager', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function(this) { + // Given the providers are loaded + server.loadFixtures('preprint-providers'); + this.store = this.owner.lookup('service:store'); + const osf = server.schema.preprintProviders.find('osf') as ModelInstance; + + // And create a user for the service with institutions + server.create('user', { id: 'institution-user' }, 'withInstitutions'); + + // And find and set the user for the service + const currentUserModel = await this.store.findRecord('user', 'institution-user'); + + this.owner.lookup('service:current-user').setProperties({ + testUser: currentUserModel, currentUserId: currentUserModel.id, + }); + + // And create a preprint with affiliated institutions + const preprintMock = server.create('preprint', { provider: osf }, 'withAffiliatedInstitutions'); + + // And retrieve the preprint from the store + const preprint: PreprintModel = await this.store.findRecord('preprint', preprintMock.id); + + this.set('affiliatedInstitutions', []); + + const managerMock = Object({ + provider: { + documentType: { + singular: 'Test Preprint Word', + }, + }, + preprint, + resetAffiliatedInstitutions: (): void => { + this.set('affiliatedInstitutions', []); + }, + isAffiliatedInstitutionsDisabled(): boolean { + return ! this.preprint.currentUserPermissions.includes(Permission.Write); + }, + isElementDisabled(): boolean { + return !(this.preprint.currentUserPermissions).includes(Permission.Admin); + }, + updateAffiliatedInstitution: (affiliatedIinstitution: InstitutionModel): void => { + const affiliatedInstitutions = this.get('affiliatedInstitutions'); + if (managerMock.isInstitutionAffiliated(affiliatedIinstitution.id)) { + affiliatedInstitutions.removeObject(affiliatedIinstitution); + } else { + affiliatedInstitutions.addObject(affiliatedIinstitution); + } + this.set('affiliatedInstitutions', affiliatedInstitutions); + + }, + isInstitutionAffiliated: (id: string): boolean => { + const affiliatedInstitutions = this.get('affiliatedInstitutions'); + return affiliatedInstitutions.find((mockInstitution: any) => mockInstitution.id === id) !== undefined; + + }, + }); + this.set('managerMock', managerMock); + }); + + test('it renders the correct labels', + async function(assert) { + + // Given the component is rendered + await render(hbs` + + + `); + + // Then the first attribute is verified by name and selected + assert.dom('[data-test-affiliated-institutions-label]').hasText('Affiliated Institutions'); + // eslint-disable-next-line max-len + assert.dom('[data-test-affiliated-institutions-description]').hasText('You can affiliate your Test Preprint Word with your institution if it is an OSF institutional member and has worked with the Center for Open Science to create a dedicated institutional OSF landing page.'); + }); + + test('it renders with 4 user institutions and 0 affiliated preprint institution - create flow', + async function(assert) { + // Given the mock is instantiated + const managerMock = this.get('managerMock'); + + // And retrieve the preprint from the store + const preprint: PreprintModel = await this.store.findRecord('preprint', managerMock.preprint.id); + // And I remove the affiliated insitutions + preprint.affiliatedInstitutions = [] as any; + await preprint.save(); + // And I remove the affiliated insitutions + managerMock.preprint.affiliatedInstitutions = []; + await managerMock.preprint.save(); + + // When the component is rendered + await render(hbs` + + + `); + + // Then the first attribute is verified by name and selected + assert.dom('[data-test-institution-name="0"]').hasText('Main OSF Test Institution'); + assert.dom('[data-test-institution-input="0"]').isChecked(); + + // And the other institutions are verified as checked + assert.dom('[data-test-institution-input="1"]').isChecked(); + assert.dom('[data-test-institution-input="2"]').isChecked(); + assert.dom('[data-test-institution-input="3"]').isChecked(); + assert.dom('[data-test-institution-input="4"]').doesNotExist(); + + // Finally the affiliatedInstitutions on the manager is verified + assert.equal(this.get('affiliatedInstitutions').length, 4); + }); + + test('it renders with 4 user institutions and 1 affiliated preprint institution - edit flow', + async function(assert) { + // Given the mock is instantiated + const managerMock = this.get('managerMock'); + + const affiliatedInstitutions = [] as any[]; + managerMock.preprint.affiliatedInstitutions.map((institution: InstitutionModel) => { + if (institution.id === 'osf') { + affiliatedInstitutions.push(institution); + } + }); + + // When the component is rendered + managerMock.preprint.affiliatedInstitutions = affiliatedInstitutions; + this.set('managerMock', managerMock); + await render(hbs` + + + `); + + // Then the first attribute is verified by name and selected + assert.dom('[data-test-institution-name="0"]').hasText('Main OSF Test Institution'); + assert.dom('[data-test-institution-input="0"]').isChecked(); + + // And the other institutions are verified as not selected + assert.dom('[data-test-institution-input="1"]').isNotChecked(); + assert.dom('[data-test-institution-input="2"]').isNotChecked(); + assert.dom('[data-test-institution-input="3"]').isNotChecked(); + assert.dom('[data-test-institution-input="4"]').doesNotExist(); + + // Finally the affiliatedInstitutions on the manager is verified + assert.equal(this.get('affiliatedInstitutions').length, 1); + }); + + test('it removes affiliated preprint institution', + async function(assert) { + // Given the component is rendered + await render(hbs` + + + `); + + // When I unclick the first affiliated preprint + await click('[data-test-institution-input="0"]'); + + // Then the first attribute is verified by name and unselected + assert.dom('[data-test-institution-name="0"]').hasText('Main OSF Test Institution'); + assert.dom('[data-test-institution-input="0"]').isNotChecked(); + + // And the other institutions are verified as not selected + assert.dom('[data-test-institution-input="1"]').isNotChecked(); + assert.dom('[data-test-institution-input="2"]').isNotChecked(); + assert.dom('[data-test-institution-input="3"]').isNotChecked(); + assert.dom('[data-test-institution-input="4"]').doesNotExist(); + + const affiliatedInstitutions = this.get('affiliatedInstitutions'); + + affiliatedInstitutions.forEach((institution: InstitutionModel) => { + assert.notEqual(institution.id, 'osf', 'The osf institution is found.'); + }); + + // Finally the affiliatedInstitutions on the manager is verified + assert.equal(this.get('affiliatedInstitutions').length, 0); + }); + + test('it adds affiliated preprint institution', + async function(assert) { + // Given the component is rendered + await render(hbs` + + + `); + + // And I find the name of the component under test + // eslint-disable-next-line max-len + const secondAffiliatedInstitutionName = this.element.querySelector('[data-test-institution-name="1"]')?.textContent?.trim(); + + // When I click the second affiliated preprint + await click('[data-test-institution-input="1"]'); + + // Then the second attribute is verified selected + assert.dom('[data-test-institution-input="1"]').isChecked(); + + // And the first institution is verified as selected + assert.dom('[data-test-institution-input="0"]').isChecked(); + // And the other institutions are verified as not selected + assert.dom('[data-test-institution-input="2"]').isNotChecked(); + assert.dom('[data-test-institution-input="3"]').isNotChecked(); + assert.dom('[data-test-institution-input="4"]').doesNotExist(); + + const affiliatedInstitutions = this.get('affiliatedInstitutions'); + + // Finally I determine if the second institutions is now affiliated + let isInstitutionAffiliatedFound = false; + affiliatedInstitutions.forEach((institution: InstitutionModel) => { + if (institution.name === secondAffiliatedInstitutionName) { + isInstitutionAffiliatedFound = true; + } + }); + + assert.true(isInstitutionAffiliatedFound, 'The second institution is now affiliated'); + + // Finally the affiliatedInstitutions on the manager is verified + assert.equal(this.get('affiliatedInstitutions').length, 2); + }); + + test('it renders with the institutions enabled for write users', + async function(assert) { + // Given the mock is instantiated + const managerMock = this.get('managerMock'); + managerMock.preprint.currentUserPermissions = [Permission.Write, Permission.Read]; + this.set('managerMock', managerMock); + + // When the component is rendered + await render(hbs` + + + `); + + // Then the first attribute is verified by name and selected + assert.dom('[data-test-institution-name="0"]').hasText('Main OSF Test Institution'); + assert.dom('[data-test-institution-input="0"]').isChecked(); + assert.dom('[data-test-institution-input="0"]').isEnabled(); + + // And the other institutions are verified as not selected + assert.dom('[data-test-institution-input="1"]').isNotChecked(); + assert.dom('[data-test-institution-input="1"]').isEnabled(); + assert.dom('[data-test-institution-input="2"]').isNotChecked(); + assert.dom('[data-test-institution-input="2"]').isEnabled(); + assert.dom('[data-test-institution-input="3"]').isNotChecked(); + assert.dom('[data-test-institution-input="3"]').isEnabled(); + assert.dom('[data-test-institution-input="4"]').doesNotExist(); + + // Finally the affiliatedInstitutions on the manager is verified + assert.equal(this.get('affiliatedInstitutions').length, 1); + }); + + test('it renders with the institutions as disabled for read users', + async function(assert) { + // Given the mock is instantiated + const managerMock = this.get('managerMock'); + managerMock.preprint.currentUserPermissions = [Permission.Read]; + this.set('managerMock', managerMock); + + // When the component is rendered + await render(hbs` + + + `); + + // Then the first attribute is verified by name and selected + assert.dom('[data-test-institution-name="0"]').hasText('Main OSF Test Institution'); + assert.dom('[data-test-institution-input="0"]').isChecked(); + assert.dom('[data-test-institution-input="0"]').isDisabled(); + + // And the other institutions are verified as not selected + assert.dom('[data-test-institution-input="1"]').isNotChecked(); + assert.dom('[data-test-institution-input="1"]').isDisabled(); + assert.dom('[data-test-institution-input="2"]').isNotChecked(); + assert.dom('[data-test-institution-input="2"]').isDisabled(); + assert.dom('[data-test-institution-input="3"]').isNotChecked(); + assert.dom('[data-test-institution-input="3"]').isDisabled(); + assert.dom('[data-test-institution-input="4"]').doesNotExist(); + + // Finally the affiliatedInstitutions on the manager is verified + assert.equal(this.get('affiliatedInstitutions').length, 1); + }); +}); diff --git a/app/preprints/-components/preprint-institutions/institution-manager/component.ts b/app/preprints/-components/preprint-institutions/institution-manager/component.ts new file mode 100644 index 0000000000..f67c374cb3 --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-manager/component.ts @@ -0,0 +1,117 @@ +import Component from '@glimmer/component'; +import { action, notifyPropertyChange } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import Intl from 'ember-intl/services/intl'; +import Toast from 'ember-toastr/services/toast'; + +import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; +import { tracked } from '@glimmer/tracking'; +import Store from '@ember-data/store'; +import CurrentUser from 'ember-osf-web/services/current-user'; +import InstitutionModel from 'ember-osf-web/models/institution'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; + + +interface PreprintInstitutionModel extends InstitutionModel { + isSelected: boolean; +} + +/** + * The Institution Manager Args + */ +interface InstitutionArgs { + manager: PreprintStateMachine; +} + +export default class InstitutionsManagerComponent extends Component { + // Required + manager = this.args.manager; + + // private properties + @service toast!: Toast; + @service intl!: Intl; + @service store!: Store; + @service currentUser!: CurrentUser; + @tracked institutions!: PreprintInstitutionModel[]; + @tracked preprintWord = this.manager.provider.documentType.singular; + + constructor(owner: unknown, args: InstitutionArgs) { + super(owner, args); + + this.manager.resetAffiliatedInstitutions(); + taskFor(this.loadInstitutions).perform(); + } + + @task + @waitFor + private async loadInstitutions() { + if (this.manager.preprint) { + try { + this.institutions = [] as PreprintInstitutionModel[]; + const userInstitutions = await this.currentUser.user!.institutions; + + await this.manager.preprint.affiliatedInstitutions; + + userInstitutions.map((institution: PreprintInstitutionModel) => { + this.institutions.push(institution); + }); + + /** + * The affiliated institutions of a preprint is in + * "edit" mode if there are institutions on the + * preprint model or the flow is in edit mode. + * Since the affiliated institutions + * are persisted by clicking the next button, the + * affiliated institutions can be in "Edit mode" even + * when the manager is not in edit mode. + */ + let isEditMode = this.manager.isEditFlow; + this.manager.preprint.affiliatedInstitutions.map((institution: PreprintInstitutionModel) => { + isEditMode = true; + if(this.isAffiliatedInstitutionOwnerByUser(institution.id)) { + institution.isSelected = true; + this.manager.updateAffiliatedInstitution(institution); + } + }); + + /** + * The business rule is during the create flow or + * "non-edit-flow" all of the institutions should be + * checked by default + */ + if (!isEditMode) { + userInstitutions.map((institution: PreprintInstitutionModel) => { + institution.isSelected = true; + this.manager.updateAffiliatedInstitution(institution); + }); + } + + notifyPropertyChange(this, 'institutions'); + + } catch (e) { + const errorMessage = this.intl.t('preprints.submit.step-metadata.institutions.load-institutions-error'); + captureException(e, { errorMessage }); + this.toast.error(getApiErrorMessage(e), errorMessage); + throw e; + } + } + } + + private isAffiliatedInstitutionOwnerByUser(id: string): boolean { + return this.institutions.find( + institution => institution.id === id, + ) !== undefined; + } + + @action + toggleInstitution(institution: PreprintInstitutionModel) { + this.manager.updateAffiliatedInstitution(institution); + } + + public get isElementDisabled(): boolean { + return this.manager.isAffiliatedInstitutionsDisabled(); + } +} diff --git a/app/preprints/-components/preprint-institutions/institution-manager/template.hbs b/app/preprints/-components/preprint-institutions/institution-manager/template.hbs new file mode 100644 index 0000000000..54e22e3368 --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-manager/template.hbs @@ -0,0 +1,6 @@ +{{yield (hash + institutions=this.institutions + toggleInstitution=this.toggleInstitution + preprintWord=this.preprintWord + isElementDisabled=this.isElementDisabled +)}} \ No newline at end of file diff --git a/app/preprints/-components/preprint-institutions/institution-select-list/component-test.ts b/app/preprints/-components/preprint-institutions/institution-select-list/component-test.ts new file mode 100644 index 0000000000..e95a67ea97 --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-select-list/component-test.ts @@ -0,0 +1,139 @@ +import { click, render} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupRenderingTest} from 'ember-qunit'; +import { module, test } from 'qunit'; +import { setupIntl } from 'ember-intl/test-support'; + + +module('Integration | Preprint | Component | Institution Manager | Institution Select List', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function(this) { + // Given the testing variables are instantiated + + this.set('toggleInstitution', []); + + + // And the manager mock is created + const managerMock = Object({ + institutions: [], + isElementDisabled: false, + toggleInstitution: (institution: any): void => { + const toggleInstitution = this.get('toggleInstitution'); + toggleInstitution.push(institution); + this.set('toggleInstitution', toggleInstitution); + }, + }); + this.set('managerMock', managerMock); + }); + + test('it does not render component without institutions', + async function(assert) { + + // Given the component is rendered + await render(hbs` + + `); + + // Then the component is not displayed + assert.dom('[data-test-affiliated-institution]').doesNotExist('The institution is displayed'); + }); + + test('it renders the component with an institution enabled and selected', + async function(assert) { + // Give manager is set-up for testing + const managerMock = this.get('managerMock'); + managerMock.institutions = [Object({ + id: 1, + isSelected: true, + name: 'The institution name', + })]; + + this.set('managerMock', managerMock); + + // When the component is rendered + await render(hbs` + + `); + + // Then the component is displayed + assert.dom('[data-test-affiliated-institution]').exists('The institution component is displayed'); + + // And the label exists + assert.dom('[data-test-affiliated-institutions-label]').hasText('Affiliated Institutions'); + + // And the description exists + // eslint-disable-next-line max-len + assert.dom('[data-test-affiliated-institutions-description]').hasText('You can affiliate your with your institution if it is an OSF institutional member and has worked with the Center for Open Science to create a dedicated institutional OSF landing page.'); + + // And the input is checked + assert.dom('[data-test-institution-input="0"]').isChecked(); + + // And the input is enabled + assert.dom('[data-test-institution-input="0"]').isEnabled(); + + // And the institution name is displayed + assert.dom('[data-test-institution-name="0"]').hasText('The institution name'); + + // Finally the institution is clicked + await click('[data-test-institution-input="0"]'); + + assert.deepEqual(this.get('toggleInstitution'), [ + { + id: 1, + isSelected: false, + name: 'The institution name', + }, + ]); + + }); + + test('it renders the component with an institution disabled and not selected', + async function(assert) { + // Give manager is set-up for testing + const managerMock = this.get('managerMock'); + managerMock.isElementDisabled = true; + managerMock.institutions = [Object({ + id: 1, + isSelected: false, + name: 'The institution name', + })]; + this.set('managerMock', managerMock); + + // When the component is rendered + await render(hbs` + + `); + + // Then the component is displayed + assert.dom('[data-test-affiliated-institution]').exists('The institution component is displayed'); + + // And the label exists + assert.dom('[data-test-affiliated-institutions-label]').hasText('Affiliated Institutions'); + + // And the description exists + // eslint-disable-next-line max-len + assert.dom('[data-test-affiliated-institutions-description]').hasText('You can affiliate your with your institution if it is an OSF institutional member and has worked with the Center for Open Science to create a dedicated institutional OSF landing page.'); + + // And the input is checked + assert.dom('[data-test-institution-input="0"]').isNotChecked(); + + // And the input is enabled + assert.dom('[data-test-institution-input="0"]').isDisabled(); + + // And the institution name is displayed + assert.dom('[data-test-institution-name="0"]').hasText('The institution name'); + + assert.deepEqual(this.get('toggleInstitution'), [ ]); + + }); +}); diff --git a/app/preprints/-components/preprint-institutions/institution-select-list/component.ts b/app/preprints/-components/preprint-institutions/institution-select-list/component.ts new file mode 100644 index 0000000000..9806d228cf --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-select-list/component.ts @@ -0,0 +1,28 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import InstitutionsManagerComponent from '../institution-manager/component'; + + +/** + * The Institution Select List Args + */ +interface InstitutionSelectListArgs { + manager: InstitutionsManagerComponent; +} + +export default class InstitutionSelectList extends Component { + @service intl!: Intl; + + // Required + manager = this.args.manager; + + public get displayComponent(): boolean { + return this.args.manager.institutions.length > 0; + } + + public get descriptionDisplay(): string { + return this.intl.t('preprints.submit.step-metadata.institutions.description', + { singularPreprintWord: this.manager.preprintWord, htmlSafe: true}) as string; + } +} diff --git a/app/preprints/-components/preprint-institutions/institution-select-list/styles.scss b/app/preprints/-components/preprint-institutions/institution-select-list/styles.scss new file mode 100644 index 0000000000..d58ad28e44 --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-select-list/styles.scss @@ -0,0 +1,44 @@ +.institution-list-container { + width: 100%; + + .institution-container { + width: 100%; + display: flex; + flex-direction: row; + justify-items: center; + align-items: flex-start; + margin-bottom: 5px; + height: 30px; + + .institution-checkbox { + margin-right: 10px; + display: flex; + flex-direction: row; + justify-items: center; + align-items: center; + padding-bottom: 4px; + height: 30px; + } + + .label { + font-weight: normal; + font-size: 14px; + display: flex; + flex-direction: row; + justify-items: center; + align-items: center; + height: 30px; + width: 100%; + } + } + + &.mobile { + .label { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 90%; + } + + } +} diff --git a/app/preprints/-components/preprint-institutions/institution-select-list/template.hbs b/app/preprints/-components/preprint-institutions/institution-select-list/template.hbs new file mode 100644 index 0000000000..8f2f4edb1b --- /dev/null +++ b/app/preprints/-components/preprint-institutions/institution-select-list/template.hbs @@ -0,0 +1,29 @@ +{{#if this.displayComponent}} +
+ +

+ {{this.descriptionDisplay}} +

+ {{#each @manager.institutions as |institution index|}} + + {{/each}} +
+{{/if}} \ No newline at end of file diff --git a/app/preprints/-components/submit/author-assertions/component.ts b/app/preprints/-components/submit/author-assertions/component.ts index 95898a9739..e22655246b 100644 --- a/app/preprints/-components/submit/author-assertions/component.ts +++ b/app/preprints/-components/submit/author-assertions/component.ts @@ -132,7 +132,6 @@ const AuthorAssertionsFormValidation: ValidationObject = { export default class PublicData extends Component{ @service intl!: Intl; @tracked isConflictOfInterestStatementDisabled = true; - @tracked isPublicDataStatementDisabled = true; authorAssertionFormChangeset = buildChangeset( this.args.manager.preprint, AuthorAssertionsFormValidation, @@ -169,7 +168,7 @@ export default class PublicData extends Component{ this.intl.t('preprints.submit.step-assertions.conflict-of-interest-none')); this.isConflictOfInterestStatementDisabled = true; } else { - this.isConflictOfInterestStatementDisabled = false; + this.isConflictOfInterestStatementDisabled = false || !this.args.manager.isAdmin(); } } @@ -177,7 +176,7 @@ export default class PublicData extends Component{ public updateCoi(): void { if (this.authorAssertionFormChangeset.get('hasCoi')) { this.authorAssertionFormChangeset.set('conflictOfInterestStatement', null); - this.isConflictOfInterestStatementDisabled = false; + this.isConflictOfInterestStatementDisabled = false || !this.args.manager.isAdmin(); } else { this.authorAssertionFormChangeset.set('conflictOfInterestStatement', this.intl.t('preprints.submit.step-assertions.conflict-of-interest-none')); @@ -198,4 +197,8 @@ export default class PublicData extends Component{ this.authorAssertionFormChangeset.execute(); this.args.manager.validateAuthorAssertions(true); } + + public get isElementDisabled(): boolean { + return this.args.manager.isElementDisabled(); + } } diff --git a/app/preprints/-components/submit/author-assertions/link-widget/component.ts b/app/preprints/-components/submit/author-assertions/link-widget/component.ts index 72b751e762..1a6c3e78e7 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/component.ts +++ b/app/preprints/-components/submit/author-assertions/link-widget/component.ts @@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking'; */ interface LinkWidgetArgs { update: (_: string[]) => {}; + disabled: boolean; links: string[]; } diff --git a/app/preprints/-components/submit/author-assertions/link-widget/link/component-test.ts b/app/preprints/-components/submit/author-assertions/link-widget/link/component-test.ts new file mode 100644 index 0000000000..b5cb3b55a9 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/component-test.ts @@ -0,0 +1,177 @@ +import { click, fillIn, render} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupRenderingTest} from 'ember-qunit'; +import { module, test } from 'qunit'; +import { setupIntl } from 'ember-intl/test-support'; + + +module('Integration | Preprint | Component | author-assertions | link-widget | link', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + const removeLinkInput: any = []; + const onUpdateData: any = []; + + hooks.beforeEach(async function(this) { + // Given the variables are reset + removeLinkInput.length = 0; + onUpdateData.length = 0; + + // When the testDataMock is instantiated + const testDataMock = Object({ + link: 'https://www.validate-url.com', + index: 1, + placeholder: 'the place holder', + removeLink(index: number): void { + removeLinkInput.push(index); + }, + onUpdate(value: string, index: number): void { + onUpdateData.push(value, index); + }, + }); + + // Then the class variables are set + this.set('testDataMock', testDataMock); + this.set('disabled', false); + }); + + test('it renders the link with a remove button when enabled', + async function(assert) { + // Given the component is rendered + await render(hbs` + `); + // Then the link value is verified + assert.dom('[data-test-link-input="1"] input').hasValue('https://www.validate-url.com'); + + // And the link placeholder is verified + assert.dom('[data-test-link-input="1"] input').hasProperty('placeholder', 'the place holder'); + + // And the link is not disabled + assert.dom('[data-test-link-input="1"] input').hasProperty('disabled', false); + + // And the button exists + assert.dom('[data-test-remove-link="1"]').exists(); + + // And the component methods are verified + assert.deepEqual(removeLinkInput, []); + assert.deepEqual(onUpdateData, ['https://www.validate-url.com', 1]); + }); + + test('it renders the link disabled without a remove button when disabled', + async function(assert) { + this.set('disabled', true); + // Given the component is rendered + await render(hbs` + `); + // Then the link value is verified + assert.dom('[data-test-link-input="1"] input').hasValue('https://www.validate-url.com'); + + // And the link placeholder is verified + assert.dom('[data-test-link-input="1"] input').hasProperty('placeholder', 'the place holder'); + + // And the link is disabled + assert.dom('[data-test-link-input="1"] input').hasProperty('disabled', true); + + // And the button does not exists + assert.dom('[data-test-remove-link="1"]').doesNotExist(); + + // And the component methods are verified + assert.deepEqual(removeLinkInput, []); + assert.deepEqual(onUpdateData, ['https://www.validate-url.com', 1]); + }); + + test('it should handle an onChange event', + async function(assert) { + // Given the component is rendered + await render(hbs` + `); + const inputElement = '[data-test-link-input="1"] input'; + // Then the link value is verified + assert.dom(inputElement).hasValue('https://www.validate-url.com'); + + // When the input value is changed + await fillIn(inputElement, 'https://new.valid-url.com'); + + // Then the input is verified + assert.dom(inputElement).hasValue('https://new.valid-url.com'); + + // And the component methods are verified + assert.deepEqual(removeLinkInput, []); + assert.deepEqual(onUpdateData, [ + 'https://www.validate-url.com', 1, 'https://new.valid-url.com', 1, 'https://new.valid-url.com', 1, + ]); + + }); + + test('it removes a link when the remove button is clicked', + async function(assert) { + // Given the component is rendered + await render(hbs` + `); + // Then the link value is verified + assert.dom('[data-test-link-input="1"] input').hasValue('https://www.validate-url.com'); + + // When the button is clicked + await click('[data-test-remove-link="1"]'); + + // Then the component methods are verified + assert.deepEqual(removeLinkInput, [1]); + assert.deepEqual(onUpdateData, ['https://www.validate-url.com', 1]); + }); + + test('it displays an error message with an invalid url', + async function(assert) { + // Given the component is rendered + await render(hbs` + `); + const inputElement = '[data-test-link-input="1"] input'; + // Then the link value is verified + assert.dom(inputElement).hasValue('https://www.validate-url.com'); + + // When the invalid value is input + await fillIn(inputElement, ''); + + // The valid the input is updated + assert.dom(inputElement).hasValue(''); + + // And the required text is visible + assert.dom('[data-test-validation-errors="value"] p').hasText('This field must be a valid url.'); + }); + +}); diff --git a/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts b/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts index 962bd531da..c317cd45b5 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts @@ -15,6 +15,7 @@ import { tracked } from '@glimmer/tracking'; interface LinkArgs { remove: (__:number) => {}; update: (_: string, __:number) => {}; + disabled: boolean; value: string; placeholder: string; index: number; diff --git a/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs b/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs index 41d1869d72..05a6db923c 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs @@ -10,8 +10,9 @@ >
- + {{#unless @disabled}} + + {{/unless}}
{{/if}} diff --git a/app/preprints/-components/submit/author-assertions/link-widget/template.hbs b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs index 619042b9db..b2bbe9fc33 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/template.hbs +++ b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs @@ -7,18 +7,21 @@ @value={{link}} @index={{index}} @placeholder={{@placeholder}} + @disabled={{@disabled}} />
{{/each}} - - \ No newline at end of file + {{#unless @disabled}} + + {{/unless}} + diff --git a/app/preprints/-components/submit/author-assertions/public-data/component.ts b/app/preprints/-components/submit/author-assertions/public-data/component.ts index 93b797b8cb..2326082b92 100644 --- a/app/preprints/-components/submit/author-assertions/public-data/component.ts +++ b/app/preprints/-components/submit/author-assertions/public-data/component.ts @@ -16,6 +16,7 @@ interface PublicDataArgs { manager: PreprintStateMachine; changeSet: BufferedChangeset; preprintWord: string; + disabled: boolean; validate: () => {}; } @@ -64,11 +65,11 @@ export default class PublicData extends Component{ public updatePublicDataOptions(): void { if (this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.AVAILABLE) { this.args.changeSet.set('whyNoData', null); - this.isPublicDataWhyNoStatementDisabled = false; + this.isPublicDataWhyNoStatementDisabled = false || !this.args.manager.isAdmin(); } else if (this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.NO) { this.args.changeSet.set('dataLinks', []); this.args.changeSet.set('whyNoData', null); - this.isPublicDataWhyNoStatementDisabled = false; + this.isPublicDataWhyNoStatementDisabled = false || !this.args.manager.isAdmin(); this.placeholder = this.intl.t('preprints.submit.step-assertions.public-data-no-placeholder'); } else { this.args.changeSet.set('dataLinks', []); diff --git a/app/preprints/-components/submit/author-assertions/public-data/template.hbs b/app/preprints/-components/submit/author-assertions/public-data/template.hbs index 5ed8f34ecf..b46f61e6f9 100644 --- a/app/preprints/-components/submit/author-assertions/public-data/template.hbs +++ b/app/preprints/-components/submit/author-assertions/public-data/template.hbs @@ -20,6 +20,7 @@ @valuePath={{'hasDataLinks'}} @class='radio-group {{if (is-mobile) 'mobile'}}' @isRequired={{true}} + @disabled={{@disabled}} @options={{this.publicDataOptions}} @onchange={{this.updatePublicDataOptions}} as |radioGroup| @@ -33,6 +34,7 @@ diff --git a/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts index f887121c0f..0176ab93ed 100644 --- a/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts @@ -16,6 +16,7 @@ interface PublicPreregistrationArgs { manager: PreprintStateMachine; changeSet: BufferedChangeset; preprintWord: string; + disabled: boolean; validate: () => {}; } @@ -96,11 +97,11 @@ export default class PublicPreregistration extends Component {{#each this.publicPreregLinkInfoOptions as |infoOption|}} @@ -54,6 +56,7 @@
diff --git a/app/preprints/-components/submit/author-assertions/template.hbs b/app/preprints/-components/submit/author-assertions/template.hbs index 9a7799624b..49200a5b68 100644 --- a/app/preprints/-components/submit/author-assertions/template.hbs +++ b/app/preprints/-components/submit/author-assertions/template.hbs @@ -29,6 +29,7 @@ @class='radio-group {{if (is-mobile) 'mobile'}}' @isRequired={{true}} @options={{this.coiOptions}} + @disabled={{this.isElementDisabled}} @onchange={{this.updateCoi}} as |radioGroup| > @@ -52,6 +53,7 @@ @changeSet={{this.authorAssertionFormChangeset}} @preprintWord={{@manager.provider.documentType.singular}} @validate={{this.validate}} + @disabled={{this.isElementDisabled}} @manager={{@manager}} />
@@ -61,6 +63,7 @@ @changeSet={{this.authorAssertionFormChangeset}} @preprintWord={{@manager.provider.documentType.singular}} @validate={{this.validate}} + @disabled={{this.isElementDisabled}} @manager={{@manager}} /> diff --git a/app/preprints/-components/submit/metadata/component.ts b/app/preprints/-components/submit/metadata/component.ts index 0a779eebff..938d324bed 100644 --- a/app/preprints/-components/submit/metadata/component.ts +++ b/app/preprints/-components/submit/metadata/component.ts @@ -79,7 +79,7 @@ const MetadataFormValidation: ValidationObject = { export default class Metadata extends Component{ @service store!: Store; metadataFormChangeset = buildChangeset(this.args.manager.preprint, MetadataFormValidation); - showAddContributorWidget = true; + showAddContributorWidget = this.args.manager.isAdmin(); @tracked displayRequiredLicenseFields = false; @tracked licenses = [] as LicenseModel[]; license!: LicenseModel; @@ -167,4 +167,8 @@ export default class Metadata extends Component{ this.metadataFormChangeset.execute(); this.args.manager.validateMetadata(true); } + + public get widgetMode(): string { + return this.args.manager.isAdmin() ? 'editable' : 'readonly'; + } } diff --git a/app/preprints/-components/submit/metadata/template.hbs b/app/preprints/-components/submit/metadata/template.hbs index 98f3cd7335..ab07104f80 100644 --- a/app/preprints/-components/submit/metadata/template.hbs +++ b/app/preprints/-components/submit/metadata/template.hbs @@ -16,11 +16,20 @@ @preprint={{@manager.preprint}} @shouldShowAdd={{this.showAddContributorWidget}} @toggleAddContributorWidget={{this.toggleAddContributorWidget}} - @widgetMode={{'editable'}} + @widgetMode={{this.widgetMode}} @displayPermissionWarning={{this.displayPermissionWarning}} /> +
+ + + +
+ +
{{#if this.isSubmit}} {{#if (is-mobile)}}
@@ -118,6 +118,38 @@
{{/if}} {{/if}} + {{!-- {{#if @manager.isEditFlow}} + {{#if (is-mobile)}} +
+ +
+ {{else}} +
+ +
+ {{/if}} + {{/if}} --}} {{#if @manager.isWithdrawalButtonDisplayed}}
{ displayAuthorAssertions = false; @tracked statusFlowIndex = 1; @tracked isEditFlow = false; + affiliatedInstitutions = [] as InstitutionModel[]; constructor(owner: unknown, args: StateMachineArgs) { super(owner, args); @@ -98,7 +100,7 @@ export default class PreprintStateMachine extends Component{ } } - this.isWithdrawalButtonDisplayed = this.preprint.currentUserPermissions.includes(Permission.Admin) && + this.isWithdrawalButtonDisplayed = this.isAdmin() && (this.preprint.reviewsState === ReviewsState.ACCEPTED || this.preprint.reviewsState === ReviewsState.PENDING) && !isWithdrawalRejected; @@ -123,6 +125,16 @@ export default class PreprintStateMachine extends Component{ await this.router.transitionTo('preprints.discover', this.provider.id); } + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onCancel(): Promise { + await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); + } + + /** * Callback for the action-flow component */ @@ -253,6 +265,23 @@ export default class PreprintStateMachine extends Component{ this.metadataValidation ) { await this.saveOnStep(); + + if (this.preprint.currentUserPermissions.includes(Permission.Write)) { + try { + await this.preprint.updateM2MRelationship( + 'affiliatedInstitutions', + this.affiliatedInstitutions, + ); + await this.preprint.reload(); + } catch (e) { + // eslint-disable-next-line max-len + const errorMessage = this.intl.t('preprints.submit.step-metadata.institutions.save-institutions-error'); + captureException(e, { errorMessage }); + this.toast.error(getApiErrorMessage(e), errorMessage); + throw e; + } + } + if (this.displayAuthorAssertions) { this.isNextButtonDisabled = !this.authorAssertionValidation; } else { @@ -624,4 +653,36 @@ export default class PreprintStateMachine extends Component{ const primaryFile = await rootFolder!.files; this.preprint.set('primaryFile', primaryFile.lastObject); } + + @action + public updateAffiliatedInstitution(institution: InstitutionModel): void { + if (this.isInstitutionAffiliated(institution.id)) { + this.affiliatedInstitutions.removeObject(institution); + } else { + this.affiliatedInstitutions.addObject(institution); + } + } + + private isInstitutionAffiliated(id: string): boolean { + return this.affiliatedInstitutions.find( + institution => institution.id === id, + ) !== undefined; + } + + @action + public resetAffiliatedInstitutions(): void { + this.affiliatedInstitutions.length = 0; + } + + public isAdmin(): boolean { + return this.preprint.currentUserPermissions.includes(Permission.Admin); + } + + public isElementDisabled(): boolean { + return !this.isAdmin(); + } + + public isAffiliatedInstitutionsDisabled(): boolean { + return !this.preprint.currentUserPermissions.includes(Permission.Write); + } } diff --git a/app/preprints/-components/submit/preprint-state-machine/template.hbs b/app/preprints/-components/submit/preprint-state-machine/template.hbs index 60ffb3451a..39b51bbad6 100644 --- a/app/preprints/-components/submit/preprint-state-machine/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/template.hbs @@ -6,6 +6,7 @@ onNext=this.onNext onPrevious=this.onPrevious onSubmit=this.onSubmit + onCancel=this.onCancel preprint=this.preprint provider=this.provider isNextButtonDisabled=this.isNextButtonDisabled @@ -38,4 +39,11 @@ statusFlowIndex=this.statusFlowIndex displayAuthorAssertions=this.displayAuthorAssertions + + updateAffiliatedInstitution=this.updateAffiliatedInstitution + resetAffiliatedInstitutions=this.resetAffiliatedInstitutions + + isAffiliatedInstitutionsDisabled=this.isAffiliatedInstitutionsDisabled + isElementDisabled=this.isElementDisabled + isAdmin=this.isAdmin )}} \ No newline at end of file diff --git a/app/preprints/-components/submit/review/template.hbs b/app/preprints/-components/submit/review/template.hbs index b37c9e00c0..88af48a698 100644 --- a/app/preprints/-components/submit/review/template.hbs +++ b/app/preprints/-components/submit/review/template.hbs @@ -76,6 +76,7 @@ />
+
diff --git a/app/preprints/detail/template.hbs b/app/preprints/detail/template.hbs index 6b8d5f867b..a6a14847d5 100644 --- a/app/preprints/detail/template.hbs +++ b/app/preprints/detail/template.hbs @@ -166,6 +166,8 @@
+ + {{#if this.model.preprint.node.links}}

{{t 'preprints.detail.supplemental_materials'}}

diff --git a/app/preprints/edit/route.ts b/app/preprints/edit/route.ts index 9773c59693..4156ead0d3 100644 --- a/app/preprints/edit/route.ts +++ b/app/preprints/edit/route.ts @@ -13,6 +13,7 @@ import PreprintEdit from 'ember-osf-web/preprints/edit/controller'; import Intl from 'ember-intl/services/intl'; import Transition from '@ember/routing/-private/transition'; import { Permission } from 'ember-osf-web/models/osf-model'; +import Toast from 'ember-toastr/services/toast'; @requireAuth() export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, {}) { @@ -21,6 +22,7 @@ export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, { @service router!: RouterService; @service intl!: Intl; @service metaTags!: MetaTags; + @service toast!: Toast; headTags?: HeadTagDef[]; // This does NOT work on chrome and I'm going to leave it just in case @@ -46,10 +48,14 @@ export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, { !preprint.currentUserPermissions.includes(Permission.Write) || preprint.isWithdrawn ) { - throw new Error('User does not have permission to edit this preprint'); + const errorMessage = this.intl.t('preprints.submit.edit-permission-error', + { + singularPreprintWord: provider.documentType.singular, + }); + this.toast.error(errorMessage); + throw new Error(errorMessage); } - return { provider, preprint, diff --git a/lib/osf-components/addon/components/contributors/card/readonly/template.hbs b/lib/osf-components/addon/components/contributors/card/readonly/template.hbs index 2505d9fc28..7447631332 100644 --- a/lib/osf-components/addon/components/contributors/card/readonly/template.hbs +++ b/lib/osf-components/addon/components/contributors/card/readonly/template.hbs @@ -36,6 +36,9 @@ data-test-contributor-permission={{@contributor.id}} local-class='permission-section' > + + {{t 'osf-components.contributors.permissionsNotEditable' }} + {{t (concat 'osf-components.contributors.permissions.' @contributor.permission)}}
diff --git a/lib/osf-components/addon/components/validated-input/text/template.hbs b/lib/osf-components/addon/components/validated-input/text/template.hbs index f08a4c45d6..28466252c8 100644 --- a/lib/osf-components/addon/components/validated-input/text/template.hbs +++ b/lib/osf-components/addon/components/validated-input/text/template.hbs @@ -21,6 +21,7 @@ @value={{this.value}} maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} + ...attributes />
{{else}} @@ -35,6 +36,7 @@ @value={{this.value}} maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} + ...attributes /> {{/if}} {{/validated-input/x-input-wrapper}} diff --git a/mirage/config.ts b/mirage/config.ts index 69ba4614ca..a8e595fa16 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -378,6 +378,17 @@ export default function(this: Server) { relatedModelName: 'file', }); + osfNestedResource(this, 'preprint', 'affiliatedInstitutions', { + path: '/preprints/:parentID/institutions/', + defaultSortKey: 'index', + relatedModelName: 'institution', + }); + + osfToManyRelationship(this, 'preprint', 'affiliatedInstitutions', { + only: ['related', 'update', 'add', 'remove'], + path: '/preprints/:parentID/relationships/institutions', + }); + this.put('/preprints/:parentID/files/:fileProviderId/upload', uploadToRoot); // Upload to file provider osfNestedResource(this, 'preprint', 'primaryFile', { diff --git a/mirage/factories/preprint.ts b/mirage/factories/preprint.ts index c8b43052c3..858e51b5a5 100644 --- a/mirage/factories/preprint.ts +++ b/mirage/factories/preprint.ts @@ -31,6 +31,7 @@ export interface PreprintTraits { acceptedWithdrawalComment: Trait; rejectedWithdrawalNoComment: Trait; reviewAction: Trait; + withAffiliatedInstitutions: Trait; } export default Factory.extend({ @@ -41,7 +42,7 @@ export default Factory.extend({ addLicenseName: true, - currentUserPermissions: [Permission.Admin], + currentUserPermissions: [Permission.Admin, Permission.Write, Permission.Read], reviewsState: ReviewsState.REJECTED, @@ -221,6 +222,23 @@ export default Factory.extend({ }, }), + withAffiliatedInstitutions: trait({ + afterCreate(preprint, server) { + const currentUser = server.schema.users.first(); + const affiliatedInstitutions = server.createList('institution', 3); + const osfInstitution = server.create('institution', { + id: 'osf', + name: 'Main OSF Test Institution', + }); + affiliatedInstitutions.unshift(osfInstitution); + + const institutions = currentUser.institutions; + institutions.models.push(osfInstitution); + currentUser.update({institutions}); + preprint.update({ affiliatedInstitutions }); + }, + }), + reviewAction: trait({ afterCreate(preprint, server) { const creator = server.create('user', { fullName: 'Review action Commentor' }); diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index f8166062d9..e2872b404e 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -15,6 +15,7 @@ import { settingsScenario } from './settings'; import { registrationsLiteScenario } from './registrations.lite'; import { registrationsManyProjectsScenario} from './registrations.many-projects'; import { userScenario } from './user'; +import { preprintsAffiliatedInstitutionsScenario } from './preprints.affiliated-institutions'; const { mirageScenarios, @@ -76,7 +77,9 @@ export default function(server: Server) { if (mirageScenarios.includes('preprints')) { preprintsScenario(server, currentUser); } - + if (mirageScenarios.includes('preprints::affiliated-institutions')) { + preprintsAffiliatedInstitutionsScenario(server, currentUser); + } if (mirageScenarios.includes('cedar')) { cedarMetadataRecordsScenario(server); } diff --git a/mirage/scenarios/preprints.affiliated-institutions.ts b/mirage/scenarios/preprints.affiliated-institutions.ts new file mode 100644 index 0000000000..128e068f04 --- /dev/null +++ b/mirage/scenarios/preprints.affiliated-institutions.ts @@ -0,0 +1,103 @@ +import { ModelInstance, Server } from 'ember-cli-mirage'; +import { Permission } from 'ember-osf-web/models/osf-model'; +import { + PreprintDataLinksEnum, + PreprintPreregLinksEnum, +} from 'ember-osf-web/models/preprint'; + +import PreprintProvider from 'ember-osf-web/models/preprint-provider'; +import { ReviewsState } from 'ember-osf-web/models/provider'; +import User from 'ember-osf-web/models/user'; +import faker from 'faker'; + +export function preprintsAffiliatedInstitutionsScenario( + server: Server, + currentUser: ModelInstance, +) { + buildOSF(server, currentUser); +} + +function buildOSF( + server: Server, + currentUser: ModelInstance, +) { + const osf = server.schema.preprintProviders.find('osf') as ModelInstance; + + const brand = server.create('brand', { + primaryColor: '#286090', + secondaryColor: '#fff', + heroLogoImage: 'images/default-brand/osf-preprints-white.png', + heroBackgroundImage: 'images/default-brand/bg-dark.jpg', + }); + + const currentUserModerator = server.create('moderator', + { id: currentUser.id, user: currentUser, provider: osf }, 'asAdmin'); + + const noAffiliatedInstitutionsPreprint = server.create('preprint', { + provider: osf, + id: 'osf-no-affiliated-institutions', + title: 'Preprint RWF: Pre-moderation, Admin and Approved', + currentUserPermissions: [Permission.Admin,Permission.Write,Permission.Read], + reviewsState: ReviewsState.ACCEPTED, + description: `${faker.lorem.sentence(200)}\n${faker.lorem.sentence(100)}`, + doi: '10.30822/artk.v1i1.79', + originalPublicationDate: new Date('2016-11-30T16:00:00.000000Z'), + preprintDoiCreated: new Date('2016-11-30T16:00:00.000000Z'), + customPublicationCitation: 'This is the publication Citation', + hasCoi: true, + conflictOfInterestStatement: 'This is the conflict of interest statement', + hasDataLinks: PreprintDataLinksEnum.NOT_APPLICABLE, + dataLinks: [ + 'http://www.datalink.com/1', + 'http://www.datalink.com/2', + 'http://www.datalink.com/3', + ], + hasPreregLinks: PreprintPreregLinksEnum.NOT_APPLICABLE, + }); + + const osfApprovedAdminIdentifier = server.create('identifier'); + + noAffiliatedInstitutionsPreprint.update({ identifiers: [osfApprovedAdminIdentifier] }); + + const affiliatedInstitutionsPreprint = server.create('preprint', { + provider: osf, + id: 'osf-affiliated-institutions', + title: 'Preprint RWF: Pre-moderation, Admin and Approved', + currentUserPermissions: [Permission.Admin,Permission.Write,Permission.Read], + reviewsState: ReviewsState.ACCEPTED, + description: `${faker.lorem.sentence(200)}\n${faker.lorem.sentence(100)}`, + doi: '10.30822/artk.v1i1.79', + originalPublicationDate: new Date('2016-11-30T16:00:00.000000Z'), + preprintDoiCreated: new Date('2016-11-30T16:00:00.000000Z'), + customPublicationCitation: 'This is the publication Citation', + hasCoi: true, + conflictOfInterestStatement: 'This is the conflict of interest statement', + hasDataLinks: PreprintDataLinksEnum.NOT_APPLICABLE, + dataLinks: [ + 'http://www.datalink.com/1', + 'http://www.datalink.com/2', + 'http://www.datalink.com/3', + ], + hasPreregLinks: PreprintPreregLinksEnum.NOT_APPLICABLE, + }, 'withAffiliatedInstitutions'); + + const subjects = server.createList('subject', 7); + + osf.update({ + allowSubmissions: true, + highlightedSubjects: subjects, + subjects, + licensesAcceptable: server.schema.licenses.all(), + // currentUser, + // eslint-disable-next-line max-len + advisory_board: '
\n

Advisory Group

\n

Our advisory group includes leaders in preprints and scholarly communication\n

\n
\n
    \n
  • Devin Berg : engrXiv, University of Wisconsin-Stout
  • \n
  • Pete Binfield : PeerJ PrePrints
  • \n
  • Benjamin Brown : PsyArXiv, Georgia Gwinnett College
  • \n
  • Philip Cohen : SocArXiv, University of Maryland
  • \n
  • Kathleen Fitzpatrick : Modern Language Association
  • \n
\n
\n
\n
    \n
  • John Inglis : bioRxiv, Cold Spring Harbor Laboratory Press
  • \n
  • Rebecca Kennison : K | N Consultants
  • \n
  • Kristen Ratan : CoKo Foundation
  • \n
  • Oya Rieger : Ithaka S+R
  • \n
  • Judy Ruttenberg : SHARE, Association of Research Libraries
  • \n
\n
\n
', + footer_links: '', + brand, + moderators: [currentUserModerator], + preprints: [ + noAffiliatedInstitutionsPreprint, + affiliatedInstitutionsPreprint, + ], + description: 'This is the description for osf', + }); +} diff --git a/mirage/scenarios/preprints.ts b/mirage/scenarios/preprints.ts index 81f44752c1..670bd1d9d0 100644 --- a/mirage/scenarios/preprints.ts +++ b/mirage/scenarios/preprints.ts @@ -68,7 +68,7 @@ function buildOSF( 'http://www.datalink.com/3', ], hasPreregLinks: PreprintPreregLinksEnum.NOT_APPLICABLE, - }); + }, 'withAffiliatedInstitutions'); const osfApprovedAdminIdentifier = server.create('identifier'); diff --git a/mirage/serializers/preprint.ts b/mirage/serializers/preprint.ts index 4856c3bf7a..7e35ba3b29 100644 --- a/mirage/serializers/preprint.ts +++ b/mirage/serializers/preprint.ts @@ -15,7 +15,24 @@ export default class PreprintSerializer extends ApplicationSerializer) { - const relationships: SerializedRelationships = {}; + const relationships: SerializedRelationships = { + contributors: { + links: { + related: { + href: `${apiUrl}/v2/preprints/${model.id}/contributors`, + meta: this.buildRelatedLinkMeta(model, 'contributors'), + }, + }, + }, + citation: { + links: { + related: { + href: `${apiUrl}/v2/preprints/${model.id}/citation/`, + meta: {}, + }, + }, + }, + }; if (model.provider) { relationships.provider = { @@ -32,12 +49,16 @@ export default class PreprintSerializer extends ApplicationSerializer { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function(this: ThisTestContext) { + server.loadFixtures('preprint-providers'); + const osf = server.schema.preprintProviders.find('osf') as ModelInstance; + + const preprintMock = server.create('preprint', { provider: osf }, 'withAffiliatedInstitutions'); + const preprintMockNoInstitutions = server.create('preprint', { provider: osf }); + + const store = this.owner.lookup('service:store'); + const preprint: PreprintModel = await store.findRecord('preprint', preprintMock.id); + const preprintNoInstitutions: PreprintModel = await store.findRecord('preprint', preprintMockNoInstitutions.id); + this.preprintMock = preprint; + this.preprintNoInstitutionsMock = preprintNoInstitutions; + }); + + test('no institutions', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').doesNotExist(); + }); + + test('many institutions', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').exists(); + assert.dom('[data-test-preprint-institution-list]').exists({ count: 4 }); + }); + + test('no institutions reviews', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').doesNotExist(); + }); + + test('many institutions reviews', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').exists(); + assert.dom('[data-test-preprint-institution-list]').exists({ count: 4 }); + }); +}); diff --git a/translations/en-us.yml b/translations/en-us.yml index 8ab788a768..62dbc33b0f 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1179,6 +1179,7 @@ preprints: paragraph: 'A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal peer review and published in a scientific journal. Learn More.' create_button: 'Create Preprint' submit: + edit-permission-error: 'User does not have permission to edit this {singularPreprintWord}' title-submit: 'New {documentType}' title-edit: 'Edit {documentType}' step-title: @@ -1215,6 +1216,11 @@ preprints: publication-doi-input: 'Publication DOI' publication-date-input: 'Publication Date' publication-citation-input: 'Publication Citation' + institutions: + label: 'Affiliated Institutions' + save-institutions-error: 'Failed to save affiliated institutions' + load-institutions-error: 'Failed to load affiliated institutions' + description: 'You can affiliate your {singularPreprintWord} with your institution if it is an OSF institutional member and has worked with the Center for Open Science to create a dedicated institutional OSF landing page.' step-assertions: title: 'Author Assertions' conflict-of-interest-input: 'Conflict of Interest' @@ -1284,6 +1290,9 @@ preprints: step-supplements: 'Supplements' step-review: 'Review' action-flow: + cancel: 'Cancel' + cancel-modal-body: 'Are you sure you want to cancel editing? The updates on this page will not be saved.' + cancel-modal-title: 'Cancel Edit' delete: 'Delete' delete-modal-body: 'Are you sure you want to delete the {singularPreprintWord}? This action CAN NOT be undone.' delete-modal-title: 'Delete {singularPreprintWord}' @@ -1341,6 +1350,7 @@ preprints: views: 'Views' metrics_disclaimer: 'Metrics collected since:' supplemental_materials: 'Supplemental Materials' + affiliated_institutions: 'Affiliated Institutions' tags: 'Tags' withdrawn_title: 'Withdrawn: {title}' reason_for_withdrawal: 'Reason for withdrawal' @@ -2691,6 +2701,7 @@ osf-components: button: 'Remove contributor' success: 'You have successfully removed {contributorName}.' errorHeading: 'Could not remove contributor. ' + permissionsNotEditable: 'Only Admins may edit permissions.' reviewActionsList: failedToLoadActions: 'Failed to load moderation history' noActionsFound: 'No moderation history found'