diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd9dddac85..9f51bf7eece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [24.07.0] - 2024-09-18 +### Added +- Preprints Affiliation Project - FE Release +- My Preprints Page: preprint card and paginated public preprint list + +## [24.06.0] - 2024-08-21 +### Added +- Misc bug and a11y fixes +- Added route for My Preprints page + +## [24.05.0] - 2024-07-08 +### Added +- Add subjects to project metadata editor +- Preprints to EOW phase 2 +### Removed +- Removed LawrXiv logo from OSF Preprints discover page + + ## [24.04.0] - 2024-04-30 ### Added - Misc bug and a11y fixes @@ -1980,6 +1998,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Quick Files +[24.05.2]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.05.2 +[24.05.1]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.05.1 +[24.05.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.05.0 +[24.04.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.04.0 [24.03.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.03.0 [24.02.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.02.0 [24.01.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.01.0 diff --git a/app/adapters/contributor.ts b/app/adapters/contributor.ts index cf70bd2d0d5..691a9b2caf1 100644 --- a/app/adapters/contributor.ts +++ b/app/adapters/contributor.ts @@ -18,11 +18,14 @@ export default class ContributorAdapter extends OsfAdapter { if (requestType === 'findRecord') { const [objectId, userId] = (id || '').split('-'); const node = this.store.peekRecord('node', objectId); + const preprint = this.store.peekRecord('preprint', objectId); const draft = this.store.peekRecord('draft-registration', objectId); let baseUrl; assert(`"contributorId" must be "objectId-userId": got ${objectId}-${userId}`, Boolean(objectId && userId)); if (node) { baseUrl = this.buildRelationshipURL((node as any)._internalModel.createSnapshot(), 'contributors'); + } else if (preprint) { + baseUrl = this.buildRelationshipURL((preprint as any)._internalModel.createSnapshot(), 'contributors'); } else { baseUrl = this.buildRelationshipURL((draft as any)._internalModel.createSnapshot(), 'contributors'); } @@ -31,13 +34,21 @@ export default class ContributorAdapter extends OsfAdapter { if (snapshot && requestType === 'createRecord') { const node = snapshot.belongsTo('node'); + const preprint = snapshot.belongsTo('preprint'); const draftRegistration = snapshot.belongsTo('draftRegistration'); const user = snapshot.belongsTo('users'); - assert('"node" or "draftRegistration" relationship is needed to create a contributor', - Boolean(node || draftRegistration)); + assert('"node" or "draftRegistration" or "preprint" relationship is needed to create a contributor', + Boolean(node || draftRegistration || preprint)); assert('"users" relationship, "email" or "fullName" is needed to create a contributor', Boolean(user || snapshot.attr('email') || snapshot.attr('fullName'))); let baseUrl; + + if (preprint) { + // if preprint relationship is defined + // we post to v2/preprints//contributors + baseUrl = this.buildRelationshipURL(preprint, 'contributors'); + } + if (node) { // if node relationship is defined // we post to v2/nodes//contributors diff --git a/app/adapters/preprint-request.ts b/app/adapters/preprint-request.ts new file mode 100644 index 00000000000..c9e3e1da640 --- /dev/null +++ b/app/adapters/preprint-request.ts @@ -0,0 +1,10 @@ +import ActionAdapter from './action'; + +export default class PreprintRequestAdapter extends ActionAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'preprint-request': PreprintRequestAdapter; + } // eslint-disable-line semi +} diff --git a/app/models/abstract-node.ts b/app/models/abstract-node.ts index f27c62fa281..2ce635d1340 100644 --- a/app/models/abstract-node.ts +++ b/app/models/abstract-node.ts @@ -1,9 +1,9 @@ import { hasMany, AsyncHasMany, attr } from '@ember-data/model'; - +import { PromiseManyArray } from '@ember-data/store/-private'; import BaseFileItem from 'ember-osf-web/models/base-file-item'; import DraftRegistrationModel from 'ember-osf-web/models/draft-registration'; import FileProviderModel from 'ember-osf-web/models/file-provider'; - +import ReviewActionModel from 'ember-osf-web/models/review-action'; import { Permission } from './osf-model'; export default class AbstractNodeModel extends BaseFileItem { @@ -15,6 +15,9 @@ export default class AbstractNodeModel extends BaseFileItem { @attr('array') currentUserPermissions!: Permission[]; + @hasMany('review-action', { inverse: 'target' }) + reviewActions!: PromiseManyArray; + } declare module 'ember-data/types/registries/model' { diff --git a/app/models/file-provider.ts b/app/models/file-provider.ts index 6e786a41dc5..2f484d51182 100644 --- a/app/models/file-provider.ts +++ b/app/models/file-provider.ts @@ -1,4 +1,5 @@ import { attr, belongsTo, AsyncBelongsTo, hasMany, AsyncHasMany } from '@ember-data/model'; +import PreprintModel from 'ember-osf-web/models/preprint'; import { Link } from 'jsonapi-typescript'; import AbstractNodeModel from './abstract-node'; @@ -24,7 +25,8 @@ export default class FileProviderModel extends BaseFileItem { @belongsTo('abstract-node', { inverse: 'files', polymorphic: true }) target!: (AsyncBelongsTo & AbstractNodeModel) | - (AsyncBelongsTo & DraftNodeModel); + (AsyncBelongsTo & DraftNodeModel) | + (AsyncBelongsTo & PreprintModel); // BaseFileItem override isProvider = true; diff --git a/app/models/license.ts b/app/models/license.ts index 75527239a18..bdeb988fb4f 100644 --- a/app/models/license.ts +++ b/app/models/license.ts @@ -9,6 +9,10 @@ export default class LicenseModel extends OsfModel { @attr('fixstring') url!: string; @attr('fixstring') text!: string; @attr('array') requiredFields!: Array; + + get hasRequiredFields(): boolean { + return this.requiredFields?.length > 0; + } } declare module 'ember-data/types/registries/model' { diff --git a/app/models/node.ts b/app/models/node.ts index 1bae1b276db..fc40dbd4141 100644 --- a/app/models/node.ts +++ b/app/models/node.ts @@ -121,6 +121,8 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col @attr('boolean') currentUserCanComment!: boolean; @attr('boolean') wikiEnabled!: boolean; + @hasMany('subject', { inverse: null, async: false }) subjectsAcceptable?: SubjectModel[]; + // FE-only property to check enabled addons. // null until getEnabledAddons has been called @tracked addonsEnabled?: string[]; diff --git a/app/models/osf-model.ts b/app/models/osf-model.ts index 435b808dbfd..5eb5c6c229b 100644 --- a/app/models/osf-model.ts +++ b/app/models/osf-model.ts @@ -1,6 +1,6 @@ import Model, { attr } from '@ember-data/model'; import Store from '@ember-data/store'; -import EmberArray, { A } from '@ember/array'; +import EmberArray, { A, isArray } from '@ember/array'; import { assert } from '@ember/debug'; import { set } from '@ember/object'; import { alias } from '@ember/object/computed'; @@ -226,6 +226,13 @@ export default class OsfModel extends Model { return this.modifyM2MRelationship('post', relationshipName, relatedModel); } + async removeM2MRelationship( + this: T, + relationshipName: RelationshipsFor & string, + ) { + return this.modifyM2MRelationship('patch', relationshipName, []); + } + async deleteM2MRelationship( this: T, relationshipName: RelationshipsFor & string, @@ -266,20 +273,26 @@ export default class OsfModel extends Model { async modifyM2MRelationship( this: T, - action: 'post' | 'delete', + action: 'post' | 'delete' | 'patch', relationshipName: RelationshipsFor & string, - relatedModel: OsfModel, + relatedModel: OsfModel | [], ) { const apiRelationshipName = underscore(relationshipName); const url = getSelfHref(this.relationshipLinks[apiRelationshipName]); - const data = JSON.stringify({ - data: [{ - id: relatedModel.id, - type: relatedModel.apiType, - }], + let data = JSON.stringify({ + data: [ ], }); + if (!isArray(relatedModel)) { + data = JSON.stringify({ + data: [{ + id: relatedModel?.id, + type: relatedModel?.apiType, + }], + }); + } + if (!url) { throw new Error(`Couldn't find self link for ${apiRelationshipName} relationship`); } @@ -353,7 +366,7 @@ export default class OsfModel extends Model { * }); * * contributors.sparseModels.forEach(contrib => { - * console.log(contrib.users.fullName); + * console.info(contrib.users.fullName); * ); * ``` */ diff --git a/app/models/preprint-provider.ts b/app/models/preprint-provider.ts index 585513cfe77..d3298cea528 100644 --- a/app/models/preprint-provider.ts +++ b/app/models/preprint-provider.ts @@ -20,6 +20,7 @@ export default class PreprintProviderModel extends ProviderModel { @attr('fixstring') email_support!: string | null; @attr('array') subjectsAcceptable!: string[]; + @attr('boolean') assertionsEnabled!: boolean; @attr('array') additionalProviders!: string[]; @attr('string') shareSource!: string; @attr('string') preprintWord!: PreprintWord; diff --git a/app/models/preprint.ts b/app/models/preprint.ts index 54405158940..dd67a495f07 100644 --- a/app/models/preprint.ts +++ b/app/models/preprint.ts @@ -1,61 +1,75 @@ import { attr, belongsTo, hasMany, AsyncBelongsTo, AsyncHasMany } from '@ember-data/model'; import { computed } from '@ember/object'; import { alias } from '@ember/object/computed'; +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'; import IdentifierModel from './identifier'; import LicenseModel from './license'; import NodeModel from './node'; -import OsfModel, { Permission } from './osf-model'; +import { Permission } from './osf-model'; import PreprintProviderModel from './preprint-provider'; import SubjectModel from './subject'; export enum PreprintDataLinksEnum { AVAILABLE = 'available', - YES = 'yes', NO = 'no', NOT_APPLICABLE = 'not_applicable', } export enum PreprintPreregLinksEnum { AVAILABLE = 'available', - YES = 'yes', NO = 'no', NOT_APPLICABLE = 'not_applicable', } -export default class PreprintModel extends OsfModel { +export enum PreprintPreregLinkInfoEnum { + PREREG_EMPTY = '', + PREREG_DESIGNS = 'prereg_designs', + PREREG_ANALYSIS = 'prereg_analysis', + PREREG_BOTH = 'prereg_both', +} + +export interface PreprintLicenseRecordModel { + copyright_holders: string[]; + year: string; +} + +export default class PreprintModel extends AbstractNodeModel { @attr('fixstring') title!: string; @attr('date') dateCreated!: Date; @attr('date') datePublished!: Date; @attr('date') dateWithdrawn!: Date; @attr('date') originalPublicationDate!: Date | null; + @attr('fixstring') customPublicationCitation!: string | null; @attr('date') dateModified!: Date; @attr('fixstring') doi!: string | null; @attr('boolean') public!: boolean; @attr('boolean') isPublished!: boolean; @attr('boolean') isPreprintOrphan!: boolean; - @attr('object') licenseRecord!: any; + @attr('object') licenseRecord!: PreprintLicenseRecordModel; @attr('string') reviewsState!: ReviewsState; @attr('string') description!: string; @attr('date') dateLastTransitioned!: Date; @attr('date') preprintDoiCreated!: Date; @attr('array') currentUserPermissions!: Permission[]; @attr('fixstringarray') tags!: string[]; - @attr('fixstring') withdrawalJustification! : string; + @attr('fixstring') withdrawalJustification!: string; @attr('boolean') hasCoi!: boolean; @attr('string') hasDataLinks!: PreprintDataLinksEnum; @attr('string') hasPreregLinks!: PreprintPreregLinksEnum; - @attr('string') conflictOfInterestStatement!: string; + @attr('string') conflictOfInterestStatement!: string | null; @attr('array') dataLinks!: string[]; @attr('array') preregLinks!: string[]; - @attr('string') whyNoData!: string; - @attr('string') whyNoPrereg!: string; + @attr('string') whyNoData!: string | null; + @attr('string') whyNoPrereg!: string | null; + @attr('string') preregLinkInfo!: PreprintPreregLinkInfoEnum; @belongsTo('node', { inverse: 'preprints' }) node!: AsyncBelongsTo & NodeModel; @@ -69,12 +83,12 @@ export default class PreprintModel extends OsfModel { @belongsTo('preprint-provider', { inverse: 'preprints' }) provider!: AsyncBelongsTo & PreprintProviderModel; + @hasMany('institution') + affiliatedInstitutions!: AsyncHasMany; + @hasMany('review-action') reviewActions!: AsyncHasMany; - @hasMany('files', { inverse: 'target'}) - files!: AsyncHasMany & FileModel; - @hasMany('contributors', { inverse: 'preprint'}) contributors!: AsyncHasMany & ContributorModel; @@ -103,7 +117,7 @@ export default class PreprintModel extends OsfModel { @computed('license', 'licenseRecord') get licenseText(): string { const text = this.license.get('text') || ''; - const { year = '', copyright_holders = [] } = this.licenseRecord; // eslint-disable-line camelcase + const { year = '', copyright_holders = [] } = this.licenseRecord; return text .replace(/({{year}})/g, year) diff --git a/app/models/provider.ts b/app/models/provider.ts index 5d9f2597d71..a5e07a6260e 100644 --- a/app/models/provider.ts +++ b/app/models/provider.ts @@ -21,7 +21,7 @@ export interface Assets { wide_white: string; } -export enum PreprintProviderReviewsWorkFlow{ +export enum PreprintProviderReviewsWorkFlow { PRE_MODERATION = 'pre-moderation', POST_MODERATION = 'post-moderation' } diff --git a/app/models/registration.ts b/app/models/registration.ts index bcb51ae4556..5ec13834004 100644 --- a/app/models/registration.ts +++ b/app/models/registration.ts @@ -4,7 +4,7 @@ import { buildValidations, validator } from 'ember-cp-validations'; import DraftRegistrationModel from 'ember-osf-web/models/draft-registration'; import ResourceModel from 'ember-osf-web/models/resource'; -import ReviewActionModel, { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; +import { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; import { RegistrationResponse } from 'ember-osf-web/packages/registration-schema'; @@ -150,9 +150,6 @@ export default class RegistrationModel extends NodeModel.extend(Validations) { @hasMany('institution', { inverse: 'registrations' }) affiliatedInstitutions!: AsyncHasMany | InstitutionModel[]; - @hasMany('review-action', { inverse: 'target' }) - reviewActions!: AsyncHasMany | ReviewActionModel[]; - @hasMany('schema-response', { inverse: 'registration' }) schemaResponses!: AsyncHasMany | SchemaResponseModel[]; diff --git a/app/models/review-action.ts b/app/models/review-action.ts index ec8eec25efe..6258d9c8227 100644 --- a/app/models/review-action.ts +++ b/app/models/review-action.ts @@ -63,7 +63,7 @@ export default class ReviewActionModel extends Action { @attr('string') fromState!: RegistrationReviewStates; @attr('string') toState!: RegistrationReviewStates; - @belongsTo('registration', { inverse: 'reviewActions', polymorphic: true }) + @belongsTo('abstract-node', { inverse: 'reviewActions', polymorphic: true }) target!: (AsyncBelongsTo & RegistrationModel ) | (AsyncBelongsTo & PreprintModel); diff --git a/app/packages/registration-schema/validations.ts b/app/packages/registration-schema/validations.ts index 3f0753cd8f4..2ed36a3bf43 100644 --- a/app/packages/registration-schema/validations.ts +++ b/app/packages/registration-schema/validations.ts @@ -11,6 +11,7 @@ import { RegistrationResponse } from 'ember-osf-web/packages/registration-schema import { SchemaBlockGroup } from 'ember-osf-web/packages/registration-schema/schema-block-group'; import { validateFileList } from 'ember-osf-web/validators/validate-response-format'; import SchemaResponseModel from 'ember-osf-web/models/schema-response'; +import PreprintModel from 'ember-osf-web/models/preprint'; type LicensedContent = DraftRegistration | NodeModel; @@ -131,7 +132,7 @@ export function validateNodeLicense() { } export function validateSubjects() { - return (_: unknown, __: unknown, ___: unknown, ____: unknown, content: DraftRegistration) => { + return (_: unknown, __: unknown, ___: unknown, ____: unknown, content: DraftRegistration | PreprintModel ) => { const subjects = content.hasMany('subjects').value(); if (!subjects || subjects.length === 0) { return { 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 00000000000..9956df04670 --- /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 00000000000..f6256c5a52d --- /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-card/component-test.ts b/app/preprints/-components/preprint-card/component-test.ts new file mode 100644 index 00000000000..0effda064de --- /dev/null +++ b/app/preprints/-components/preprint-card/component-test.ts @@ -0,0 +1,49 @@ +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl, TestContext } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import { OsfLinkRouterStub } from 'ember-osf-web/tests/integration/helpers/osf-link-router-stub'; + +module('Integration | Component | preprint-card', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(function(this: TestContext) { + this.store = this.owner.lookup('service:store'); + this.intl = this.owner.lookup('service:intl'); + }); + + test('it renders', async function(this: TestContext, assert) { + this.owner.unregister('service:router'); + this.owner.register('service:router', OsfLinkRouterStub); + const preprint = server.create('preprint', { + tags: ['a', 'b', 'c'], + description: 'Through the night', + }); + server.create('contributor', { preprint, index: 0, bibliographic: true }); + server.create('contributor', { preprint, index: 1, bibliographic: true }); + server.create('contributor', { preprint, index: 2, bibliographic: true }); + const preprintModel = await this.store.findRecord( + 'preprint', preprint.id, { include: ['bibliographic_contributors'] }, + ); + this.set('preprint', preprintModel); + + await render(hbs` + + `); + assert.dom('[data-test-preprint-title]').exists('Preprint title exists'); + assert.dom('[data-test-preprint-title]').hasText(preprintModel.title, 'Node title is corrent'); + assert.dom('[data-test-contributors-label]').exists('Contributors label exists'); + assert.dom('[data-test-contributors-label]').hasText( + this.intl.t('node_card.contributors'), + 'Contributors label is correct', + ); + }); +}); diff --git a/app/preprints/-components/preprint-card/component.ts b/app/preprints/-components/preprint-card/component.ts new file mode 100644 index 00000000000..eb39f3ceae8 --- /dev/null +++ b/app/preprints/-components/preprint-card/component.ts @@ -0,0 +1,30 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; +import config from 'ember-osf-web/config/environment'; + +import { layout } from 'ember-osf-web/decorators/component'; +import Preprint from 'ember-osf-web/models/preprint'; +import pathJoin from 'ember-osf-web/utils/path-join'; +import { Permission } from 'ember-osf-web/models/osf-model'; + +import template from './template'; +import styles from './styles'; + +const { OSF: { url: baseURL } } = config; + +@layout(template, styles) +@tagName('') +export default class PreprintCard extends Component { + + preprint?: Preprint; + delete?: (preprint: Preprint) => void; + showTags = false; + readOnly = false; + + searchUrl = pathJoin(baseURL, 'search'); + + get shouldShowUpdateButton() { + return this.preprint && this.preprint.currentUserPermissions.includes(Permission.Admin); + } + +} diff --git a/app/preprints/-components/preprint-card/styles.scss b/app/preprints/-components/preprint-card/styles.scss new file mode 100644 index 00000000000..359632e2008 --- /dev/null +++ b/app/preprints/-components/preprint-card/styles.scss @@ -0,0 +1,121 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-card { + width: 100%; + margin: 1px 0; + + .card-contents { + display: block; + flex-direction: row; + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + + .heading { + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: flex-start; + align-items: flex-start; + width: 100%; + + :global .ember-content-placeholders-heading__title { + height: 1em; + margin-top: 5px; + margin-bottom: 5px; + + &:first-child { + width: 100%; + } + } + } + } +} + +.label-danger { + background-color: $brand-danger; +} + +.heading > span { + line-height: 1.5; +} + +.label-info { + background-color: darken($brand-info, 15%); +} + +.label-primary { + background-color: #337ab7; +} + +.label { + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: 700; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25em; + display: block; + margin-top: 5px; +} + +.osf-link { + margin-left: 5px; + margin-top: 2px; + font-weight: bold; +} + +.osf-link.mobile { + margin-left: 0; +} + +.body { + width: 80%; + + &.mobile { + width: 90%; + } +} + +dl { + margin-bottom: 10px; + + div { + display: flex; + + dt { + width: 110px; + margin-right: 5px; + } + + dd { + flex: 1; + } + } +} + +.tags { + margin-top: 2px; +} + +.link { + composes: Button from 'osf-components/components/button/styles.scss'; + composes: SecondaryButton from 'osf-components/components/button/styles.scss'; + composes: MediumButton from 'osf-components/components/button/styles.scss'; + + &:hover { + text-decoration: none !important; + } +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.update-button { + color: $color-text-blue-dark; +} diff --git a/app/preprints/-components/preprint-card/template.hbs b/app/preprints/-components/preprint-card/template.hbs new file mode 100644 index 00000000000..f41cc10294d --- /dev/null +++ b/app/preprints/-components/preprint-card/template.hbs @@ -0,0 +1,114 @@ +
+
+
+

+ {{#unless @preprint.public}} + + + + {{t 'preprints.preprint_card.private_tooltip'}} + + | + {{/unless}} + + {{@preprint.title}} + +
+ {{#if (eq @preprint.reviewsState 'pending')}} + {{t 'preprints.preprint_card.statuses.pending'}} + {{else if (eq @preprint.reviewsState 'accepted')}} + {{t 'preprints.preprint_card.statuses.accepted'}} + {{else if (eq @preprint.reviewsState 'rejected')}} + {{t 'preprints.preprint_card.statuses.rejected'}} + {{/if}} +
+

+
+
+
+
+ {{t 'preprints.preprint_card.provider'}} +
+
+ {{@preprint.provider.name}} +
+
+
+
+ {{t 'preprints.preprint_card.date_created'}} +
+
+ {{moment-format @preprint.dateCreated 'YYYY-MM-DD'}} +
+
+
+
+ {{t 'preprints.preprint_card.date_modified'}} +
+
+ {{moment-format @preprint.dateModified 'YYYY-MM-DD'}} +
+
+
+
+ {{t 'preprints.preprint_card.contributors'}} +
+
+ +
+
+ {{#if (and this.showTags @preprint.tags)}} +
+
+ {{t 'preprints.preprint_card.tags'}} +
+
+ +
+
+ {{/if}} +
+
+ + {{t 'preprints.preprint_card.view_button'}} + + {{#if this.shouldShowUpdateButton}} + + {{t 'preprints.preprint_card.update_button'}} + + {{/if}} +
+
+
+
+
diff --git a/app/preprints/-components/preprint-coi/component.ts b/app/preprints/-components/preprint-coi/component.ts new file mode 100644 index 00000000000..06317c524b6 --- /dev/null +++ b/app/preprints/-components/preprint-coi/component.ts @@ -0,0 +1,20 @@ +import Component from '@glimmer/component'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +interface CoiArgs { + preprint: PreprintModel; +} + +export default class PreprintCoi extends Component { + @service intl!: Intl; + + preprint = this.args.preprint; + + get coiDisplay(): string { + return this.preprint.hasCoi + ? this.preprint.conflictOfInterestStatement as string + : this.intl.t('preprints.submit.step-review.no-conflict-of-interest'); + } +} diff --git a/app/preprints/-components/preprint-coi/template.hbs b/app/preprints/-components/preprint-coi/template.hbs new file mode 100644 index 00000000000..8f57a8d6f51 --- /dev/null +++ b/app/preprints/-components/preprint-coi/template.hbs @@ -0,0 +1,8 @@ +
+

+ {{t 'preprints.submit.step-review.conflict-of-interest'}} +

+ + {{this.coiDisplay}} + +
\ No newline at end of file diff --git a/app/preprints/-components/preprint-existing-node-widget/component.ts b/app/preprints/-components/preprint-existing-node-widget/component.ts new file mode 100644 index 00000000000..56f90899459 --- /dev/null +++ b/app/preprints/-components/preprint-existing-node-widget/component.ts @@ -0,0 +1,21 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import NodeModel from 'ember-osf-web/models/node'; + +/** + * The Existing Node Args + */ +interface ExistingNodeArgs { + projectSelected: (_: NodeModel) => {}; +} + +/** + * The Supplements Component + */ +export default class PreprintExistingNodeWidget extends Component{ + + @action + public projectSelected(node: NodeModel): void { + this.args.projectSelected(node); + } +} diff --git a/app/preprints/-components/preprint-existing-node-widget/styles.scss b/app/preprints/-components/preprint-existing-node-widget/styles.scss new file mode 100644 index 00000000000..546c5a7cee7 --- /dev/null +++ b/app/preprints/-components/preprint-existing-node-widget/styles.scss @@ -0,0 +1,40 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + margin-bottom: 20px; + + .required { + color: $brand-danger; + } + } + + .button-container { + margin-top: 10px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + .btn-width { + width: calc(50% - 20px); + } + } + + .select-preprint-container { + width: 100%; + } + + &.mobile { + height: fit-content; + } +} + diff --git a/app/preprints/-components/preprint-existing-node-widget/template.hbs b/app/preprints/-components/preprint-existing-node-widget/template.hbs new file mode 100644 index 00000000000..d3511b1f66a --- /dev/null +++ b/app/preprints/-components/preprint-existing-node-widget/template.hbs @@ -0,0 +1,18 @@ +
+

+ {{t 'preprints.submit.step-supplements.choose-project'}} + * +

+

+ {{t 'preprints.submit.step-supplements.choose-project-line-one-description'}} +
+ {{t 'preprints.submit.step-supplements.choose-project-line-two-description'}} +

+
+ +
+
\ No newline at end of file diff --git a/app/preprints/-components/preprint-file-display/styles.scss b/app/preprints/-components/preprint-file-display/styles.scss new file mode 100644 index 00000000000..9b53779b34b --- /dev/null +++ b/app/preprints/-components/preprint-file-display/styles.scss @@ -0,0 +1,29 @@ +.display { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + + .text { + width: calc(100% - 50px); + height: 30px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + padding: 6px 0; + } + + .action { + width: 50px; + height: 30px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .blue-button { + color: $brand-info; + } + } +} diff --git a/app/preprints/-components/preprint-file-display/template.hbs b/app/preprints/-components/preprint-file-display/template.hbs new file mode 100644 index 00000000000..67654181b89 --- /dev/null +++ b/app/preprints/-components/preprint-file-display/template.hbs @@ -0,0 +1,28 @@ +
+

+ {{t 'preprints.submit.step-file.uploaded-file-title' singularPreprintWord = @preprintWord }} +

+
+
+ {{ @file.name }} +
+ {{#if @addNewFile}} +
+ + + {{t 'preprints.submit.step-file.delete-modal-button-tooltip'}} + +
+ {{/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 00000000000..77c9263ec0b --- /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 00000000000..f67c374cb3b --- /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 00000000000..54e22e33681 --- /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 00000000000..e95a67ea97e --- /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 00000000000..9806d228cfa --- /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 00000000000..d58ad28e440 --- /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 00000000000..8f2f4edb1b2 --- /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/preprint-license/styles.scss b/app/preprints/-components/preprint-license/styles.scss index ddf500d88e3..a21c8b68ec9 100644 --- a/app/preprints/-components/preprint-license/styles.scss +++ b/app/preprints/-components/preprint-license/styles.scss @@ -9,7 +9,7 @@ overflow: auto; display: block; padding: 9.5px; - margin: 0 0 10px; + margin: 10px 0; line-height: 1.42857; word-wrap: break-word; background-color: $bg-light; diff --git a/app/preprints/-components/preprint-node-display/component.ts b/app/preprints/-components/preprint-node-display/component.ts new file mode 100644 index 00000000000..c249c6e6ec2 --- /dev/null +++ b/app/preprints/-components/preprint-node-display/component.ts @@ -0,0 +1,49 @@ +import Component from '@glimmer/component'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import Store from '@ember-data/store'; +import { taskFor } from 'ember-concurrency-ts'; +import NodeModel from 'ember-osf-web/models/node'; + +interface NodeDisplayArgs { + preprint: PreprintModel; + preprintWord: string; + removeNode: () => boolean; +} + +export default class PreprintNodeDisplay extends Component { + @service intl!: Intl; + @service store!: Store; + + preprint = this.args.preprint; + node?: NodeModel; + nodeId?: string; + + + constructor(owner: unknown, args: NodeDisplayArgs) { + super(owner, args); + + this.nodeId = this.preprint.belongsTo('node').id(); + taskFor(this.loadNode).perform(); + } + + get nodeDisplay(): string { + if (this.nodeId) { + return this.node?.title as string; + } else { + return this.intl.t('preprints.submit.step-review.supplement-na', + { singularPreprintWord: this.args.preprintWord}); + } + } + + @task + @waitFor + private async loadNode() { + if (this.nodeId) { + this.node = await this.store.findRecord('node', this.nodeId); + } + } +} diff --git a/app/preprints/-components/preprint-node-display/styles.scss b/app/preprints/-components/preprint-node-display/styles.scss new file mode 100644 index 00000000000..7e8403f47da --- /dev/null +++ b/app/preprints/-components/preprint-node-display/styles.scss @@ -0,0 +1,26 @@ +.display { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + + .text { + width: calc(100% - 50px); + height: 30px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + padding: 6px 0; + } + + .action { + width: 50px; + height: 30px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + +} diff --git a/app/preprints/-components/preprint-node-display/template.hbs b/app/preprints/-components/preprint-node-display/template.hbs new file mode 100644 index 00000000000..9fee8fa1902 --- /dev/null +++ b/app/preprints/-components/preprint-node-display/template.hbs @@ -0,0 +1,30 @@ +
+ {{#if this.loadNode.isRunning}} + + {{else}} +

+ {{t 'preprints.submit.step-review.supplement-title'}} +

+
+
+ {{ this.nodeDisplay}} +
+ {{#if @removeNode}} +
+ + + {{t 'preprints.submit.step-supplements.delete-warning'}} + +
+ {{/if}} +
+ {{/if}} +
diff --git a/app/preprints/-components/preprint-provider-selection/component.ts b/app/preprints/-components/preprint-provider-selection/component.ts new file mode 100644 index 00000000000..01958beb3ae --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/component.ts @@ -0,0 +1,48 @@ +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { serviceLinks } from 'ember-osf-web/const/service-links'; +import { tracked } from '@glimmer/tracking'; +import Component from '@glimmer/component'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import RouterService from '@ember/routing/router-service'; + + +interface InputArgs { + submissionProviders: PreprintProviderModel[]; +} + + +export default class PreprintProviderSelection extends Component { + @service router!: RouterService; + + submissionProviders: PreprintProviderModel[] = this.args.submissionProviders; + learnMoreUrl: string = serviceLinks.preprintsSupport; + @tracked selectedProvider?: PreprintProviderModel; + + public get isDisabled(): boolean { + return !this.selectedProvider; + } + + public get selectedProviderId(): string | undefined { + if (this.selectedProvider) { + return this.selectedProvider.id; + } + return undefined; + } + + @action + onCreateButtonClick(): void { + if (this.selectedProvider !== undefined) { + this.router.transitionTo('preprints.submit', this.selectedProvider.id); + } + } + + @action + updateSelectedProvider(provider: PreprintProviderModel): void { + if (this.selectedProvider?.id !== provider.id) { + this.selectedProvider = provider; + } else { + this.selectedProvider = undefined; + } + } +} diff --git a/app/preprints/-components/preprint-provider-selection/preprint-provider-display/component.ts b/app/preprints/-components/preprint-provider-selection/preprint-provider-display/component.ts new file mode 100644 index 00000000000..6db051ccc63 --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/preprint-provider-display/component.ts @@ -0,0 +1,25 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; + + +interface InputArgs { + provider: PreprintProviderModel; + updateSelectedProvider: (provider: PreprintProviderModel) => void; + selectedProviderId: string; +} + + +export default class PreprintProviderDisplay extends Component { + @tracked provider: PreprintProviderModel = this.args.provider; + + public get isSelected(): boolean { + return this.args.provider.id === this.args.selectedProviderId; + } + + @action + onProviderSelect(): void { + this.args.updateSelectedProvider(this.provider); + } +} diff --git a/app/preprints/-components/preprint-provider-selection/preprint-provider-display/styles.scss b/app/preprints/-components/preprint-provider-selection/preprint-provider-display/styles.scss new file mode 100644 index 00000000000..7882443c962 --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/preprint-provider-display/styles.scss @@ -0,0 +1,58 @@ +.provider-container { + width: 265px; + height: 175px; + margin: 10px; + padding: 10px; + border: 1px solid $color-border-gray; + border-radius: 5px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + + .image-container { + width: 60px; + height: 60px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + .image { + width: 40px; + height: 40px; + } + } + + .name-container { + width: 245px; + height: 20px; + margin-top: 5px; + font-weight: bold; + text-align: center; + } + + .button-container { + width: 100%; + height: 26px; + margin-top: 36px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + } + + &:hover { + border-color: $color-border-blue-dark; + background-color: $color-bg-gray-blue-light; + } + + &.mobile { + width: 300px; + } + + &.selected { + border-color: $color-border-blue-dark; + background-color: $color-bg-gray-blue-light; + } +} diff --git a/app/preprints/-components/preprint-provider-selection/preprint-provider-display/template.hbs b/app/preprints/-components/preprint-provider-selection/preprint-provider-display/template.hbs new file mode 100644 index 00000000000..6a02ede7b2e --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/preprint-provider-display/template.hbs @@ -0,0 +1,42 @@ +
+
+ {{this.provider.name}} + +
+
+ {{this.provider.name}} +
+
+ +
+ + {{#if this.provider.description}} + {{html-safe this.provider.description}} + {{else}} + {{html-safe this.provider.name}} + {{/if}} + +
diff --git a/app/preprints/-components/preprint-provider-selection/styles.scss b/app/preprints/-components/preprint-provider-selection/styles.scss new file mode 100644 index 00000000000..4d548532d43 --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/styles.scss @@ -0,0 +1,96 @@ +// stylelint-disable max-nesting-depth + +@import 'app/styles/layout'; + +.provider-selection-container { + @include clamp-width; + width: 100%; + display: flex; + padding: 15px 0; + flex-direction: column; + justify-content: center; + align-items: flex-start; + font-style: normal; + + .heading-container { + width: 100%; + padding: 15px 0; + + .heading { + margin: 5px 10px; + font-size: 24px; + font-weight: bold; + } + } + + .paragraph-container { + width: 100%; + padding: 15px 0; + + .paragraph { + margin: 5px 10px; + font-size: 16px; + font-weight: 400; + } + } + + .provider-list-container { + width: 100%; + padding: 15px 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-content: flex-start; + } + + .create-button-container { + width: 100%; + padding: 15px 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + .create-button { + width: 175px; + height: 40px; + border-radius: 5px; + background-color: $color-bg-blue-dark; + font-size: 16px; + font-weight: bold; + color: $color-text-white; + border-style: hidden; + + &:hover { + background-color: $color-bg-blue-highlight; + } + + &.disabled { + background-color: $color-bg-gray; + } + } + } + + &.mobile { + min-width: 330px; + + .heading-container { + .heading { + margin: 5px 20px; + font-size: 20px; + } + } + + .paragraph-container { + .paragraph { + margin: 5px 20px; + } + } + + .provider-list-container { + justify-content: center; + align-content: center; + } + } +} diff --git a/app/preprints/-components/preprint-provider-selection/template.hbs b/app/preprints/-components/preprint-provider-selection/template.hbs new file mode 100644 index 00000000000..5cf71c094b0 --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/template.hbs @@ -0,0 +1,39 @@ +
+
+

+ {{t 'preprints.select.heading'}} +

+
+
+

+ {{t 'preprints.select.paragraph' link=this.learnMoreUrl htmlSafe=true}} +

+
+
+ {{#each this.submissionProviders as |provider| }} + + {{/each}} +
+
+ +
+
diff --git a/app/preprints/-components/preprint-public-data/component.ts b/app/preprints/-components/preprint-public-data/component.ts new file mode 100644 index 00000000000..82789ab5681 --- /dev/null +++ b/app/preprints/-components/preprint-public-data/component.ts @@ -0,0 +1,26 @@ +import Component from '@glimmer/component'; +import PreprintModel, { PreprintDataLinksEnum } from 'ember-osf-web/models/preprint'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +interface PublicDataArgs { + preprint: PreprintModel; + preprintWord: string; +} + +export default class PreprintPublicData extends Component { + @service intl!: Intl; + + preprint = this.args.preprint; + + get publicDataDisplay(): string { + if (this.preprint.hasDataLinks === PreprintDataLinksEnum.NOT_APPLICABLE) { + return this.intl.t('preprints.submit.step-assertions.public-data-na-placeholder', + { singularPreprintWord: this.args.preprintWord}); + } else if (this.preprint.hasDataLinks === PreprintDataLinksEnum.NO) { + return this.preprint.whyNoData as string; + } else { + return ''; + } + } +} diff --git a/app/preprints/-components/preprint-public-data/styles.scss b/app/preprints/-components/preprint-public-data/styles.scss new file mode 100644 index 00000000000..869711cd34e --- /dev/null +++ b/app/preprints/-components/preprint-public-data/styles.scss @@ -0,0 +1,15 @@ +.display { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .text { + height: 30px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + } +} diff --git a/app/preprints/-components/preprint-public-data/template.hbs b/app/preprints/-components/preprint-public-data/template.hbs new file mode 100644 index 00000000000..08ce332abd9 --- /dev/null +++ b/app/preprints/-components/preprint-public-data/template.hbs @@ -0,0 +1,16 @@ +
+

+ {{t 'preprints.submit.step-review.public-data'}} +

+
+ {{#each this.preprint.dataLinks as | link| }} +
+ {{link}} +
+ {{else}} + + {{this.publicDataDisplay}} + + {{/each}} +
+
diff --git a/app/preprints/-components/preprint-public-preregistration/component.ts b/app/preprints/-components/preprint-public-preregistration/component.ts new file mode 100644 index 00000000000..867c7228e9b --- /dev/null +++ b/app/preprints/-components/preprint-public-preregistration/component.ts @@ -0,0 +1,42 @@ +import Component from '@glimmer/component'; +import PreprintModel, { PreprintPreregLinkInfoEnum, PreprintPreregLinksEnum } from 'ember-osf-web/models/preprint'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +interface PublicPreregistrationArgs { + preprint: PreprintModel; + preprintWord: string; +} + +export default class PreprintPublicPreregistration extends Component { + @service intl!: Intl; + + preprint = this.args.preprint; + + get displayPreregLinkInfo(): boolean { + return this.preprint.hasPreregLinks === PreprintPreregLinksEnum.AVAILABLE || + this.preprint.hasPreregLinks === PreprintPreregLinksEnum.YES; + } + + get preregLinkInfoDisplay(): string { + if (this.preprint.preregLinkInfo === PreprintPreregLinkInfoEnum.PREREG_DESIGNS) { + return this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-designs'); + + } else if (this.preprint.preregLinkInfo === PreprintPreregLinkInfoEnum.PREREG_ANALYSIS) { + return this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-analysis'); + } else { + return this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-both'); + } + } + + get publicPreregistrationDisplay(): string { + if (this.preprint.hasPreregLinks === PreprintPreregLinksEnum.NOT_APPLICABLE) { + return this.intl.t('preprints.submit.step-assertions.public-data-na-placeholder', + { singularPreprintWord: this.args.preprintWord}); + } else if (this.preprint.hasPreregLinks === PreprintPreregLinksEnum.NO) { + return this.preprint.whyNoPrereg as string; + } else { + return ''; + } + } +} diff --git a/app/preprints/-components/preprint-public-preregistration/styles.scss b/app/preprints/-components/preprint-public-preregistration/styles.scss new file mode 100644 index 00000000000..869711cd34e --- /dev/null +++ b/app/preprints/-components/preprint-public-preregistration/styles.scss @@ -0,0 +1,15 @@ +.display { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .text { + height: 30px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + } +} diff --git a/app/preprints/-components/preprint-public-preregistration/template.hbs b/app/preprints/-components/preprint-public-preregistration/template.hbs new file mode 100644 index 00000000000..ed86c1e9192 --- /dev/null +++ b/app/preprints/-components/preprint-public-preregistration/template.hbs @@ -0,0 +1,21 @@ +
+

+ {{t 'preprints.submit.step-review.public-preregistration'}} +

+
+ {{#if this.displayPreregLinkInfo}} +
+ {{ this.preregLinkInfoDisplay}} +
+ {{#each this.preprint.preregLinks as | link| }} +
+ {{link}} +
+ {{/each}} + {{else}} + + {{this.publicPreregistrationDisplay}} + + {{/if}} +
+
diff --git a/app/preprints/-components/preprint-status-banner/component.ts b/app/preprints/-components/preprint-status-banner/component.ts index 6a187f9216c..d492063adf5 100644 --- a/app/preprints/-components/preprint-status-banner/component.ts +++ b/app/preprints/-components/preprint-status-banner/component.ts @@ -193,6 +193,7 @@ export default class PreprintStatusBanner extends Component{ }); const latestRequestAction = requestActions.firstObject; + // @ts-ignore: ActionTrigger does not exist on type 'never' if (latestRequestAction && latestRequestAction.actionTrigger === 'reject') { this.isWithdrawalRejected = true; this.latestAction = latestRequestAction; diff --git a/app/preprints/-components/submit/author-assertions/component.ts b/app/preprints/-components/submit/author-assertions/component.ts new file mode 100644 index 00000000000..e22655246bb --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/component.ts @@ -0,0 +1,204 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { action } from '@ember/object'; +import { ValidationObject } from 'ember-changeset-validations'; +import { validatePresence } from 'ember-changeset-validations/validators'; +import buildChangeset from 'ember-osf-web/utils/build-changeset'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { tracked } from '@glimmer/tracking'; +import { RadioButtonOption } from 'osf-components/components/form-controls/radio-button-group/component'; +import { PreprintDataLinksEnum, PreprintPreregLinksEnum } from 'ember-osf-web/models/preprint'; + + +/** + * The Author Assertions Args + */ +interface AuthorAssertionsArgs { + manager: PreprintStateMachine; +} + +interface AuthorAssertionsForm { + hasCoi: boolean; + conflictOfInterestStatement: string; + hasDataLinks: string; + whyNoData: string; + dataLinks: string[]; + hasPreregLinks: string; + whyNoPrereg: string; + preregLinks: string[]; + preregLinkInfo: PreprintPreregLinksEnum; +} + +const AuthorAssertionsFormValidation: ValidationObject = { + hasCoi: validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + conflictOfInterestStatement: validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + hasDataLinks: validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + whyNoData: [(key: string, newValue: string, oldValue: string, changes: any, content: any) => { + if (changes['hasDataLinks'] !== PreprintDataLinksEnum.AVAILABLE && + content['hasDataLinks'] !== PreprintDataLinksEnum.AVAILABLE) { + return validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + })(key, newValue, oldValue, changes, content); + } + return true; + }], + dataLinks: [(_key: string, newValue: string[], _oldValue: string[], changes: any, _content: any) => { + if (changes['hasDataLinks'] === PreprintDataLinksEnum.AVAILABLE || newValue) { + let isValid = false; + if (newValue) { + isValid = true; + newValue.map((link: string) => { + isValid = isValid && (typeof link === 'string' && link.length > 0); + }); + } + + return isValid ? true : { + context: { + type: 'empty', + }, + }; + } else { + return true; + } + }], + hasPreregLinks: validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + whyNoPrereg: [(key: string, newValue: string, oldValue: string, changes: any, content: any) => { + if ( + changes['hasPreregLinks'] !== PreprintPreregLinksEnum.AVAILABLE && + content['hasPreregLinks'] !== PreprintPreregLinksEnum.AVAILABLE + ) { + return validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + })(key, newValue, oldValue, changes, content); + } + return true; + }], + preregLinks: [(_key: string, newValue: string[], _oldValue: string[], changes: any, _content: any) => { + if (changes['hasPreregLinks'] === PreprintPreregLinksEnum.AVAILABLE || newValue) { + let isValid = false; + if (newValue) { + isValid = true; + newValue.map((link: string) => { + isValid = isValid && (typeof link === 'string' && link.length > 0); + }); + } + + return isValid ? true : { + context: { + type: 'empty', + }, + }; + } else { + return true; + } + }], + preregLinkInfo: [(key: string, newValue: string, oldValue: string, changes: any, content: any) => { + if (changes['hasPreregLinks'] === PreprintPreregLinksEnum.AVAILABLE || newValue) { + return validatePresence({ + presence: true, + ignoreBlank: false, + type: 'empty', + })(key, newValue, oldValue, changes, content); + } else { + return true; + } + }], +}; + +/** + * The Public Data Component + */ +export default class PublicData extends Component{ + @service intl!: Intl; + @tracked isConflictOfInterestStatementDisabled = true; + authorAssertionFormChangeset = buildChangeset( + this.args.manager.preprint, + AuthorAssertionsFormValidation, + ); + + coiOptions= [ + { + inputValue: true, + displayText: this.intl.t('general.yes'), + } as RadioButtonOption, + { + inputValue: false, + displayText: this.intl.t('general.no'), + } as RadioButtonOption, + ]; + + constructor(owner: unknown, args: AuthorAssertionsArgs) { + super(owner, args); + + if(this.args.manager.preprint.hasDataLinks === PreprintDataLinksEnum.NOT_APPLICABLE) { + this.authorAssertionFormChangeset.set('whyNoData', + this.intl.t('preprints.submit.step-assertions.public-data-na-placeholder', + { singularPreprintWord: this.args.manager.provider.documentType.singular})); + } + + if(this.args.manager.preprint.hasPreregLinks === PreprintPreregLinksEnum.NOT_APPLICABLE) { + this.authorAssertionFormChangeset.set('whyNoPrereg', + this.intl.t('preprints.submit.step-assertions.public-preregistration-na-placeholder', + { singularPreprintWord: this.args.manager.provider.documentType.singular})); + } + + if (this.args.manager.preprint.hasCoi === false) { + this.authorAssertionFormChangeset.set('conflictOfInterestStatement', + this.intl.t('preprints.submit.step-assertions.conflict-of-interest-none')); + this.isConflictOfInterestStatementDisabled = true; + } else { + this.isConflictOfInterestStatementDisabled = false || !this.args.manager.isAdmin(); + } + } + + @action + public updateCoi(): void { + if (this.authorAssertionFormChangeset.get('hasCoi')) { + this.authorAssertionFormChangeset.set('conflictOfInterestStatement', null); + this.isConflictOfInterestStatementDisabled = false || !this.args.manager.isAdmin(); + } else { + this.authorAssertionFormChangeset.set('conflictOfInterestStatement', + this.intl.t('preprints.submit.step-assertions.conflict-of-interest-none')); + this.isConflictOfInterestStatementDisabled = true; + } + + this.validate(); + + } + + @action + public validate(): void { + this.authorAssertionFormChangeset.validate(); + if (this.authorAssertionFormChangeset.isInvalid) { + this.args.manager.validateAuthorAssertions(false); + return; + } + 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 new file mode 100644 index 00000000000..1a6c3e78e73 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/component.ts @@ -0,0 +1,61 @@ +import Component from '@glimmer/component'; +import { action, notifyPropertyChange } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { tracked } from '@glimmer/tracking'; + + +/** + * The Data Link Widget Args + */ +interface LinkWidgetArgs { + update: (_: string[]) => {}; + disabled: boolean; + links: string[]; +} + +/** + * The Data Link Widget Component + */ +export default class LinkWidget extends Component{ + @service intl!: Intl; + @tracked links: string[] = []; + + constructor(owner: unknown, args: LinkWidgetArgs) { + super(owner, args); + + if (this.args.links?.length > 0) { + this.links = this.args.links; + this.notifyPropertyChange(); + } else { + this.addLink(); + } + } + + private notifyPropertyChange(): void { + this.args.update(this.links); + notifyPropertyChange(this, 'links'); + } + + @action + public onUpdate(value: string, index: number): void { + this.links[index] = value; + this.notifyPropertyChange(); + } + + @action + public addLink(): void { + this.links.push(''); + this.notifyPropertyChange(); + } + + @action + public removeLink(index: number): void { + if (index === 0 && this.links.length === 1) { + this.onUpdate('', index); + } else { + this.links.splice(index, 1); + this.notifyPropertyChange(); + } + } +} 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 00000000000..b5cb3b55a9f --- /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 new file mode 100644 index 00000000000..c317cd45b59 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts @@ -0,0 +1,66 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { ValidationObject } from 'ember-changeset-validations'; + +import buildChangeset from 'ember-osf-web/utils/build-changeset'; +import { validateUrlWithProtocols } from 'ember-osf-web/validators/url-with-protocol'; +import { tracked } from '@glimmer/tracking'; + + +/** + * The Data Link Args + */ +interface LinkArgs { + remove: (__:number) => {}; + update: (_: string, __:number) => {}; + disabled: boolean; + value: string; + placeholder: string; + index: number; +} + +interface LinkForm { + value: string; +} + +/** + * The Data Link Component + */ +export default class Link extends Component{ + @service intl!: Intl; + @tracked linkFormChangeset: any = null; + + linkFormValidation: ValidationObject = { + value: validateUrlWithProtocols({ + translationArgs: { description: this.intl.t('validationErrors.description') }, + }), + }; + + @action + initializeChangeset() { + this.linkFormChangeset = buildChangeset( + {value: this.args.value || undefined}, + this.linkFormValidation, + ); + + this.onUpdate(); + } + + @action + public async onUpdate(): Promise { + this.linkFormChangeset.validate(); + if (this.linkFormChangeset.isInvalid) { + this.args.update('', this.args.index); + return; + } + this.linkFormChangeset.execute(); + await this.args.update(this.linkFormChangeset.get('value'), this.args.index); + } + + @action + public async removeLink(): Promise { + await this.args.remove(this.args.index); + } +} diff --git a/app/preprints/-components/submit/author-assertions/link-widget/link/styles.scss b/app/preprints/-components/submit/author-assertions/link-widget/link/styles.scss new file mode 100644 index 00000000000..7af742af926 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/styles.scss @@ -0,0 +1,31 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.form-container { + width: 100%; + margin-top: 20px; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + + .input-container { + width: calc(100% - 50px); + + .input { + width: 100%; + } + } + + .delete-container { + display: flex; + justify-content: center; + width: 50px; + height: 34px; + flex-direction: row; + align-items: center; + + .delete { + color: $brand-danger; + } + } +} 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 new file mode 100644 index 00000000000..05a6db923cc --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs @@ -0,0 +1,40 @@ +
+ {{#if this.linkFormChangeset}} + +
+ + +
+
+ {{#unless @disabled}} + + {{/unless}} +
+
+ {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/author-assertions/link-widget/styles.scss b/app/preprints/-components/submit/author-assertions/link-widget/styles.scss new file mode 100644 index 00000000000..570b076965a --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/styles.scss @@ -0,0 +1,19 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + + +.data-link-container { + .data-link { + margin-bottom: 20px; + } + + .add-another-link { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + .plus-icon { + margin-right: 10px; + } + } +} diff --git a/app/preprints/-components/submit/author-assertions/link-widget/template.hbs b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs new file mode 100644 index 00000000000..b2bbe9fc33d --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs @@ -0,0 +1,27 @@ +
+ {{#each this.links as |link index|}} +
+ +
+ {{/each}} + + {{#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 new file mode 100644 index 00000000000..2326082b922 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-data/component.ts @@ -0,0 +1,86 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { tracked } from '@glimmer/tracking'; +import { BufferedChangeset } from 'ember-changeset/types'; +import { PreprintDataLinksEnum } from 'ember-osf-web/models/preprint'; +import { RadioButtonOption } from 'osf-components/components/form-controls/radio-button-group/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; + + +/** + * The Public Data Args + */ +interface PublicDataArgs { + manager: PreprintStateMachine; + changeSet: BufferedChangeset; + preprintWord: string; + disabled: boolean; + validate: () => {}; +} + +/** + * The Public Data Component + */ +export default class PublicData extends Component{ + @service intl!: Intl; + @tracked isPublicDataWhyNoStatementDisabled = true; + @tracked placeholder!: string; + + publicDataOptions = [ + { + inputValue: PreprintDataLinksEnum.AVAILABLE, + displayText: this.intl.t('general.available'), + } as RadioButtonOption, + { + inputValue: PreprintDataLinksEnum.NO, + displayText: this.intl.t('general.no'), + } as RadioButtonOption, + { + inputValue: PreprintDataLinksEnum.NOT_APPLICABLE, + displayText: this.intl.t('general.not-applicable'), + } as RadioButtonOption, + ]; + + public get displayPublicDataWhyNoStatement(): boolean { + return this.args.changeSet.get('hasDataLinks') === null ? + false : + !this.displayPublicDataLinks; + } + + public get displayPublicDataLinks(): boolean { + return this.args.changeSet.get('hasDataLinks') === null ? + false : + this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.AVAILABLE; + } + + @action + public updatePublicDataLinks(links: string[]): void { + this.args.changeSet.set('dataLinks', links); + this.args.validate(); + } + + @action + public updatePublicDataOptions(): void { + if (this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.AVAILABLE) { + this.args.changeSet.set('whyNoData', null); + 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.args.manager.isAdmin(); + this.placeholder = this.intl.t('preprints.submit.step-assertions.public-data-no-placeholder'); + } else { + this.args.changeSet.set('dataLinks', []); + this.isPublicDataWhyNoStatementDisabled = true; + this.args.changeSet.set('whyNoData', + this.intl.t('preprints.submit.step-assertions.public-data-na-placeholder', + { singularPreprintWord: this.args.preprintWord})); + this.placeholder = this.intl.t('preprints.submit.step-assertions.public-data-na-placeholder', + { singularPreprintWord: this.args.preprintWord}); + } + + this.args.validate(); + } +} diff --git a/app/preprints/-components/submit/author-assertions/public-data/styles.scss b/app/preprints/-components/submit/author-assertions/public-data/styles.scss new file mode 100644 index 00000000000..8156cf278c2 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-data/styles.scss @@ -0,0 +1,19 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .input-container { + margin-top: 20px; + + &.textarea-container { + * > textarea { + height: 75px; + } + } + } +} diff --git a/app/preprints/-components/submit/author-assertions/public-data/template.hbs b/app/preprints/-components/submit/author-assertions/public-data/template.hbs new file mode 100644 index 00000000000..b46f61e6f9b --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-data/template.hbs @@ -0,0 +1,56 @@ +
+ + {{#let (unique-id 'publicData') as |publicDataId|}} + +

+ {{t 'preprints.submit.step-assertions.public-data-description'}} +

+ + + {{radioGroup}} + + {{/let}} + + {{#if this.displayPublicDataLinks}} +
+ +
+ {{/if}} + + {{#if this.displayPublicDataWhyNoStatement}} +
+ +
+ {{/if}} +
+
\ No newline at end of file diff --git a/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts new file mode 100644 index 00000000000..0176ab93ede --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts @@ -0,0 +1,125 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { tracked } from '@glimmer/tracking'; +import { BufferedChangeset } from 'ember-changeset/types'; +import { PreprintPreregLinkInfoEnum, PreprintPreregLinksEnum } from 'ember-osf-web/models/preprint'; +import { RadioButtonOption } from 'osf-components/components/form-controls/radio-button-group/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; + + +/** + * The Public Preregistration Args + */ +interface PublicPreregistrationArgs { + manager: PreprintStateMachine; + changeSet: BufferedChangeset; + preprintWord: string; + disabled: boolean; + validate: () => {}; +} + +interface PreregistationLinkInfoOption { + key: string; + value: string; +} + +/** + * The Public Preregistration Component + */ +export default class PublicPreregistration extends Component{ + @service intl!: Intl; + @tracked isPublicPreregistrationWhyNoStatementDisabled = true; + @tracked placeholder!: string; + @tracked selectedValue!: string; + + constructor(owner: unknown, args: PublicPreregistrationArgs) { + super(owner, args); + + this.selectedValue = this.args.manager.preprint.preregLinkInfo; + } + + publicPreregLinkInfoOptions = [ + { + key: PreprintPreregLinkInfoEnum.PREREG_EMPTY, + value: this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-placeholder'), + } as PreregistationLinkInfoOption, + { + key: PreprintPreregLinkInfoEnum.PREREG_DESIGNS, + value: this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-designs'), + } as PreregistationLinkInfoOption, + { + key: PreprintPreregLinkInfoEnum.PREREG_ANALYSIS, + value: this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-analysis'), + } as PreregistationLinkInfoOption, + { + key: PreprintPreregLinkInfoEnum.PREREG_BOTH, + value: this.intl.t('preprints.submit.step-assertions.public-preregistration-link-info-both'), + } as PreregistationLinkInfoOption, + ]; + + + publicPreregistrationOptions = [ + { + inputValue: PreprintPreregLinksEnum.AVAILABLE, + displayText: this.intl.t('general.available'), + } as RadioButtonOption, + { + inputValue: PreprintPreregLinksEnum.NO, + displayText: this.intl.t('general.no'), + } as RadioButtonOption, + { + inputValue: PreprintPreregLinksEnum.NOT_APPLICABLE, + displayText: this.intl.t('general.not-applicable'), + } as RadioButtonOption, + ]; + + public get displayPublicPreregistrationWhyNoStatement(): boolean { + return this.args.changeSet.get('hasPreregLinks') === null ? + false : + !this.displayPublicPreregistrationLinks; + } + + public get displayPublicPreregistrationLinks(): boolean { + return this.args.changeSet.get('hasPreregLinks') === null ? + false : + this.args.changeSet.get('hasPreregLinks') === PreprintPreregLinksEnum.AVAILABLE; + } + + @action + public updatePublicPreregistrationLinks(links: string[]): void { + this.args.changeSet.set('preregLinks', links); + this.args.validate(); + } + + @action + public updatePublicPreregistrationOptions(): void { + if (this.args.changeSet.get('hasPreregLinks') === PreprintPreregLinksEnum.AVAILABLE) { + this.args.changeSet.set('whyNoPrereg', null); + this.isPublicPreregistrationWhyNoStatementDisabled = false || !this.args.manager.isAdmin(); + } else if (this.args.changeSet.get('hasPreregLinks') === PreprintPreregLinksEnum.NO) { + this.args.changeSet.set('preregLinks', []); + this.args.changeSet.set('whyNoPrereg', null); + this.isPublicPreregistrationWhyNoStatementDisabled = false || !this.args.manager.isAdmin(); + this.placeholder = this.intl.t('preprints.submit.step-assertions.public-preregistration-no-placeholder'); + } else { + this.isPublicPreregistrationWhyNoStatementDisabled = true; + this.args.changeSet.set('preregLinks', []); + this.args.changeSet.set('whyNoPrereg', + this.intl.t('preprints.submit.step-assertions.public-preregistration-na-placeholder', + { singularPreprintWord: this.args.preprintWord})); + this.placeholder = this.intl.t('preprints.submit.step-assertions.public-preregistration-na-placeholder', + { singularPreprintWord: this.args.preprintWord}); + } + + this.args.validate(); + } + + @action + public updatePreregistrationLinkInfo(linkInfo: string): void { + this.selectedValue = linkInfo; + this.args.changeSet.set('preregLinkInfo', linkInfo); + this.args.validate(); + } +} diff --git a/app/preprints/-components/submit/author-assertions/public-preregistration/styles.scss b/app/preprints/-components/submit/author-assertions/public-preregistration/styles.scss new file mode 100644 index 00000000000..f05c87cbd5c --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/styles.scss @@ -0,0 +1,30 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .input-container { + margin-top: 20px; + + .select { + display: flex; + width: calc(100% - 50px); + } + + .validation-error { + display: block; + margin-top: 5px; + margin-bottom: 10px; + } + + &.textarea-container { + * > textarea { + height: 75px; + } + } + } +} diff --git a/app/preprints/-components/submit/author-assertions/public-preregistration/template.hbs b/app/preprints/-components/submit/author-assertions/public-preregistration/template.hbs new file mode 100644 index 00000000000..284259a2050 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/template.hbs @@ -0,0 +1,79 @@ +
+ + {{#let (unique-id 'publicPreregistration') as |publicPreregistrationId|}} + +

+ {{t 'preprints.submit.step-assertions.public-preregistration-description'}} +

+ + + {{radioGroup}} + + {{/let}} + + {{#if this.displayPublicPreregistrationLinks}} +
+ + + +
+
+ +
+ {{/if}} + + {{#if this.displayPublicPreregistrationWhyNoStatement}} +
+ +
+ {{/if}} +
+
\ No newline at end of file diff --git a/app/preprints/-components/submit/author-assertions/styles.scss b/app/preprints/-components/submit/author-assertions/styles.scss new file mode 100644 index 00000000000..c954bfd6dba --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/styles.scss @@ -0,0 +1,63 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + margin-bottom: 20px; + } + + .form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .input-container { + margin-bottom: 20px; + + &.textarea-container { + * > textarea { + height: 75px; + } + } + } + } + + &.mobile { + height: fit-content; + } +} + + +:global(.radio-group) { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + + div { + width: fit-content; + margin-left: 10px; + height: 30px; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + label { + margin-top: 9px; + margin-left: 10px; + } + } +} + +:global(.radio-group.mobile) { + flex-direction: column; +} diff --git a/app/preprints/-components/submit/author-assertions/template.hbs b/app/preprints/-components/submit/author-assertions/template.hbs new file mode 100644 index 00000000000..49200a5b681 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/template.hbs @@ -0,0 +1,72 @@ +
+

+ {{t 'preprints.submit.step-assertions.title'}} +

+
+ + +
+ {{#let (unique-id 'conflictOfInterest') as |conflictOfInterestId|}} + +

+ {{t 'preprints.submit.step-assertions.conflict-of-interest-description'}} +

+ + + {{radioGroup}} + + {{/let}} +
+ + + +
+ +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/app/preprints/-components/submit/component.ts b/app/preprints/-components/submit/component.ts new file mode 100644 index 00000000000..f850222cf65 --- /dev/null +++ b/app/preprints/-components/submit/component.ts @@ -0,0 +1,43 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine, { PreprintStatusTypeEnum } from + 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; + +/** + * The Submit Args + */ +interface SubmitArgs { + manager: PreprintStateMachine; +} + +/** + * The Submit component + */ +export default class Submit extends Component{ + public get isTitleAndAbstractActive(): boolean { + return this.isSelected(PreprintStatusTypeEnum.titleAndAbstract); + } + + public get isFileActive(): boolean { + return this.isSelected(PreprintStatusTypeEnum.file); + } + + public get isMetadataActive(): boolean { + return this.isSelected(PreprintStatusTypeEnum.metadata); + } + + public get isAuthorAssertionsActive(): boolean { + return this.isSelected(PreprintStatusTypeEnum.authorAssertions); + } + + public get isSupplementsActive(): boolean { + return this.isSelected(PreprintStatusTypeEnum.supplements); + } + + public get isReviewActive(): boolean { + return this.isSelected(PreprintStatusTypeEnum.review); + } + + private isSelected(type: string): boolean { + return this.args.manager.isSelected(type); + } +} diff --git a/app/preprints/-components/submit/file/component.ts b/app/preprints/-components/submit/file/component.ts new file mode 100644 index 00000000000..64ee093aaff --- /dev/null +++ b/app/preprints/-components/submit/file/component.ts @@ -0,0 +1,127 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { taskFor } from 'ember-concurrency-ts'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import FileModel from 'ember-osf-web/models/file'; +import NodeModel from 'ember-osf-web/models/node'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +/** + * The File Args + */ +interface FileArgs { + manager: PreprintStateMachine; +} + +/** + * The File Component + */ +export default class PreprintFile extends Component{ + @service intl!: Intl; + + @tracked isFileUploadDisplayed = false; + @tracked isProjectSelectDisplayed = false; + @tracked isFileSelectDisplayed = false; + @tracked isFileAttached = false; + @tracked isEdit = false; + @tracked dragging = false; + @tracked file!: any; + @tracked selectedProjectNode!: NodeModel; + + constructor(owner: unknown, args: FileArgs) { + super(owner, args); + + taskFor(this.loadFiles).perform(); + } + + @task + @waitFor + private async loadFiles() { + const file = await this.args.manager.preprint.primaryFile; + if(file) { + this.file = file; + this.isFileAttached = true; + this.isEdit = true; + this.args.manager.validateFile(true); + } + } + + public get isSelectProjectButtonDisplayed(): boolean { + return !this.args.manager.isEditFlow; + } + + public get isSelectProjectButtonDisabled(): boolean { + return this.isButtonDisabled || this.isEdit; + } + + @action + public async validate(file: FileModel): Promise { + this.isEdit = true; + this.file = file; + this.isFileAttached = true; + this.isProjectSelectDisplayed = false; + this.isFileUploadDisplayed = false; + this.args.manager.validateFile(true); + } + + @action + public displayFileUpload(): void { + this.isFileUploadDisplayed = true; + this.isProjectSelectDisplayed = false; + this.isFileSelectDisplayed = false; + } + + @action + public displayFileSelect(): void { + this.isFileUploadDisplayed = false; + this.isProjectSelectDisplayed = true; + this.isFileSelectDisplayed = false; + } + + public get isButtonDisabled(): boolean { + return this.isProjectSelectDisplayed || this.isFileUploadDisplayed; + } + + @action + public async addNewfile(): Promise { + this.file = null; + this.isFileAttached = false; + this.isFileUploadDisplayed = false; + this.isProjectSelectDisplayed = false; + this.isFileSelectDisplayed = false; + this.args.manager.validateFile(false); + } + + @action + public onCancelSelectAction(): void { + this.isFileUploadDisplayed = false; + this.isProjectSelectDisplayed = false; + } + + @action + public projectSelected(node: NodeModel): void { + this.selectedProjectNode = node; + this.isFileSelectDisplayed= true; + } + + @task + @waitFor + async onSelectFile(file: FileModel): Promise { + await taskFor(this.args.manager.addProjectFile).perform(file); + this.validate(file); + } + + public get getSelectExplanationText(): string { + return this.intl.t('preprints.submit.step-file.project-select-explanation', + { singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized }); + } + + public get getUploadText(): string { + return this.intl.t('preprints.submit.step-file.upload-title', + { singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized }); + } +} diff --git a/app/preprints/-components/submit/file/styles.scss b/app/preprints/-components/submit/file/styles.scss new file mode 100644 index 00000000000..f35d214256c --- /dev/null +++ b/app/preprints/-components/submit/file/styles.scss @@ -0,0 +1,96 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + margin-bottom: 20px; + } + + .file-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + + .file { + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + + .upload-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .button-container { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 20px; + + .btn { + width: calc(50% - 10px); + } + + .selected { + background-color: $secondary-blue; + color: $color-text-white; + } + } + + .upload-file { + border: 1px solid $color-border-gray; + height: 150px; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + + &.highlight { + border: 1px solid $color-bg-blue-dark; + box-shadow: 2px 2px 5px $color-bg-blue-dark; + background-color: lighten($brand-success, 50%); + } + } + + .cancel-button-container { + margin-top: 10px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + } + } + + &.mobile { + .upload-container { + .button-container { + flex-direction: column; + justify-content: flex-start; + + .btn { + width: 100%; + margin-bottom: 20px; + } + } + } + } +} + diff --git a/app/preprints/-components/submit/file/template.hbs b/app/preprints/-components/submit/file/template.hbs new file mode 100644 index 00000000000..4b7a5615e5e --- /dev/null +++ b/app/preprints/-components/submit/file/template.hbs @@ -0,0 +1,130 @@ +
+

+ {{t 'preprints.submit.step-file.title'}} +

+ {{#if this.isSelectProjectButtonDisplayed}} +

+ {{t 'preprints.submit.step-file.upload-warning'}} +

+ {{/if}} + {{#if this.loadFiles.isRunning}} + + {{else}} + {{#if this.isFileAttached}} +
+
+ +
+
+ {{else}} +
+ +
+ + {{#if this.isSelectProjectButtonDisplayed}} + + {{/if}} +
+ {{#if this.isFileUploadDisplayed}} + {{#let (unique-id 'preprint-upload-files-dropzone') as |id|}} + +
+
+ {{ t 'preprints.submit.step-file.file-upload-label-one'}} +
+
+ {{ t 'preprints.submit.step-file.file-upload-label-two'}} +
+
+
+ {{/let}} + + {{/if}} + {{#if this.isProjectSelectDisplayed}} + {{ t 'preprints.submit.step-file.file-select-label'}} + + + {{#if this.isFileSelectDisplayed}} + + {{/if}} + {{/if}} + {{#if this.isButtonDisabled}} +
+ +
+ {{/if}} +
+ {{/if}} + {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/file/upload-file/component.ts b/app/preprints/-components/submit/file/upload-file/component.ts new file mode 100644 index 00000000000..393bee7b310 --- /dev/null +++ b/app/preprints/-components/submit/file/upload-file/component.ts @@ -0,0 +1,102 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import Toast from 'ember-toastr/services/toast'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import FileModel from 'ember-osf-web/models/file'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import { taskFor } from 'ember-concurrency-ts'; + +interface PreprintUploadArgs { + manager: PreprintStateMachine; + preprint: PreprintModel; + allowVersioning: boolean; + isEdit: boolean; + validate: (_: FileModel) => {}; + clickableElementId: string; + dragEnter: () => {}; + dragLeave: () => {}; + dragOver: () => {}; +} + +export default class PreprintUpload extends Component { + @service intl!: Intl; + @service toast!: Toast; + url?: URL; + rootFolder?: FileModel; + primaryFile: FileModel | undefined; + + constructor(owner: unknown, args: any) { + super(owner, args); + + taskFor(this.prepUrl).perform(); + } + + get clickableElementSelectors() { + if (this.args.clickableElementId) { + return [`#${this.args.clickableElementId}`]; + } + return []; + } + + get dropzoneOptions() { + const uploadLimit = 1; + return { + createImageThumbnails: false, + method: 'PUT', + withCredentials: true, + preventMultipleFiles: true, + acceptDirectories: false, + autoProcessQueue: true, + autoQueue: true, + parallelUploads: uploadLimit, + maxFilesize: 10000000, + timeout: null, + }; + } + + @task + @waitFor + async prepUrl() { + let urlString: string; + const theFiles = await this.args.preprint.files; + const rootFolder = await theFiles.firstObject!.rootFolder; + if(this.args.isEdit) { + this.primaryFile = await this.args.preprint.primaryFile; + urlString = this.primaryFile?.links?.upload as string; + } else { + urlString = await theFiles.firstObject!.links.upload as string; + } + + this.url = new URL( urlString ); + this.rootFolder = rootFolder; + } + + @action + buildUrl(files: any[]): string { + const { name } = files[0]; + this.url!.searchParams.append('kind', 'file'); + if(!this.args.isEdit) { + this.url!.searchParams.append('name', name); + } + return this.url!.toString(); + } + + @task + @waitFor + async success(_: any, __:any, file: FileModel): Promise { + if (this.args.isEdit) { + if (file.name !== this.primaryFile?.name) { + await this.primaryFile?.rename(file.name); + } + } else { + const primaryFile = await this.rootFolder!.files; + this.args.manager.preprint.set('primaryFile', primaryFile.firstObject); + await this.args.manager.preprint.save(); + } + this.args.validate(file); + } +} diff --git a/app/preprints/-components/submit/file/upload-file/styles.scss b/app/preprints/-components/submit/file/upload-file/styles.scss new file mode 100644 index 00000000000..db6ddffaa68 --- /dev/null +++ b/app/preprints/-components/submit/file/upload-file/styles.scss @@ -0,0 +1,8 @@ +.upload-file-widget { + height: 150px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} diff --git a/app/preprints/-components/submit/file/upload-file/template.hbs b/app/preprints/-components/submit/file/upload-file/template.hbs new file mode 100644 index 00000000000..0060cc323a3 --- /dev/null +++ b/app/preprints/-components/submit/file/upload-file/template.hbs @@ -0,0 +1,25 @@ +{{#if (or this.preUrl.isRunning this.success.isRunning)}} + +{{else}} +
+ {{#let (unique-id 'upload-files-dropzone') as |id|}} + + {{yield}} + + {{/let}} +
+{{/if}} \ No newline at end of file diff --git a/app/preprints/-components/submit/metadata/component.ts b/app/preprints/-components/submit/metadata/component.ts new file mode 100644 index 00000000000..938d324bedf --- /dev/null +++ b/app/preprints/-components/submit/metadata/component.ts @@ -0,0 +1,174 @@ + +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { action } from '@ember/object'; +import { ValidationObject } from 'ember-changeset-validations'; +import { validateFormat, validatePresence } from 'ember-changeset-validations/validators'; +import buildChangeset from 'ember-osf-web/utils/build-changeset'; +import { DOIRegex } from 'ember-osf-web/utils/doi'; +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; +import { taskFor } from 'ember-concurrency-ts'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import LicenseModel from 'ember-osf-web/models/license'; +import { tracked } from '@glimmer/tracking'; +import SubjectModel from 'ember-osf-web/models/subject'; +import { validateSubjects } from 'ember-osf-web/packages/registration-schema/validations'; +import PreprintModel, { PreprintLicenseRecordModel } from 'ember-osf-web/models/preprint'; + +/** + * The Metadata Args + */ +interface MetadataArgs { + manager: PreprintStateMachine; +} + +interface MetadataForm { + doi: string; + originalPublicationDate: number; + license: LicenseModel; + licenseCopyrights: string[]; + licenseYear: string; + subjects: SubjectModel[]; +} + +const MetadataFormValidation: ValidationObject = { + doi: validateFormat({ + allowBlank: true, + allowNone: true, + ignoreBlank: true, + regex: DOIRegex, + type: 'invalid_doi', + }), + license: validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + licenseCopyrights: [(key: string, newValue: string, oldValue: string, changes: any, content: any) => { + if (changes['license'] && changes['license']?.requiredFields?.length > 0) { + return validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + })(key, newValue, oldValue, changes, content); + } + return true; + }], + licenseYear: [(key: string, newValue: string, oldValue: string, changes: any, content: any) => { + if (changes['license'] && changes['license']?.requiredFields?.length > 0) { + const yearRegex = /^((?!(0))[0-9]{4})$/; + + return validateFormat({ + allowBlank: false, + allowNone: false, + ignoreBlank: false, + regex: yearRegex, + type: 'year_format', + })(key, newValue, oldValue, changes, content); + } + return true; + }], + subjects: validateSubjects(), +}; + +/** + * The Metadata Component + */ +export default class Metadata extends Component{ + @service store!: Store; + metadataFormChangeset = buildChangeset(this.args.manager.preprint, MetadataFormValidation); + showAddContributorWidget = this.args.manager.isAdmin(); + @tracked displayRequiredLicenseFields = false; + @tracked licenses = [] as LicenseModel[]; + license!: LicenseModel; + preprint!: PreprintModel; + originalPublicationDateMin = new Date(1900, 0, 1); + today = new Date(); + originalPublicationDateMax = new Date( + this.today.getFullYear(), + this.today.getMonth(), + this.today.getDate(), + ); + + constructor(owner: unknown, args: MetadataArgs) { + super(owner, args); + + this.preprint = this.args.manager.preprint; + taskFor(this.loadLicenses).perform(); + } + + get displayPermissionWarning(): boolean { + return !this.args.manager.isEditFlow; + } + + @task + @waitFor + private async loadLicenses() { + this.licenses = await this.args.manager.provider.queryHasMany('licensesAcceptable', { + page: { size: 100 }, + sort: 'name', + }); + + this.license = await this.preprint.license; + this.setLicenseFields(); + } + + @action + toggleAddContributorWidget() { + this.showAddContributorWidget = !this.showAddContributorWidget; + } + + private setLicenseFields(): void { + if (this.license?.hasRequiredFields) { + this.metadataFormChangeset.set('licenseCopyrights', + this.preprint.licenseRecord.copyright_holders.join(' ')); + this.metadataFormChangeset.set('licenseYear', this.preprint.licenseRecord.year); + + } + this.displayRequiredLicenseFields = this.license?.hasRequiredFields; + } + + private setHasRequiredFields(): void { + this.license = this.metadataFormChangeset.get('license'); + this.displayRequiredLicenseFields = this.license?.hasRequiredFields || false; + } + + private updateLicenseRecord(): void { + if (this.metadataFormChangeset.get('license').hasRequiredFields) { + this.metadataFormChangeset.set('licenseRecord', { + copyright_holders: [this.metadataFormChangeset.get('licenseCopyrights')], + year: this.metadataFormChangeset.get('licenseYear'), + + } as PreprintLicenseRecordModel); + } else { + this.metadataFormChangeset.set('licenseRecord', undefined); + } + } + + @action + public async hasSubjects(hasSubjects: boolean): Promise { + if (hasSubjects) { + this.validate(); + } + } + + @action + public validate(): void { + this.setHasRequiredFields(); + this.metadataFormChangeset.validate(); + if (this.metadataFormChangeset.isInvalid) { + this.args.manager.validateMetadata(false); + return; + } + + this.updateLicenseRecord(); + 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/styles.scss b/app/preprints/-components/submit/metadata/styles.scss new file mode 100644 index 00000000000..8b1b8c5fff1 --- /dev/null +++ b/app/preprints/-components/submit/metadata/styles.scss @@ -0,0 +1,50 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + margin-bottom: 20px; + } + + .form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .input-container { + margin-bottom: 20px; + + .tags-border { + input { + padding: 6px 12px; + border: 1px solid $color-border-gray; + font-size: 14px; + height: 34px; + } + } + + .read-only { + input { + background-color: $color-bg-white; + } + } + } + } + + &.mobile { + height: fit-content; + } +} + +.TagsWidget :global(.emberTagInput-tag) { + cursor: default; + +} diff --git a/app/preprints/-components/submit/metadata/template.hbs b/app/preprints/-components/submit/metadata/template.hbs new file mode 100644 index 00000000000..ab07104f80d --- /dev/null +++ b/app/preprints/-components/submit/metadata/template.hbs @@ -0,0 +1,170 @@ +
+

+ {{t 'preprints.submit.step-metadata.title'}} +

+ {{#if this.loadLicenses.isRunning}} + + {{else}} +
+
+ + +
+ +
+ + + +
+ + + {{#let (unique-id 'license') as |licenseId|}} + +

+ {{t 'preprints.submit.step-metadata.license-description' htmlSafe=true}} +

+ + {{license.name}} + + + {{#if this.displayRequiredLicenseFields}} + + + + {{/if}} + {{/let}} +
+ {{#let (unique-id) 'subjects' as |subjectsFieldId|}} + + + + + + {{/let}} +
+
+ {{#let (unique-id) 'tags' as |tagsFieldId|}} + + + {{/let}} +
+ + + +
+ +
+ + +
+
+ {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/component.ts b/app/preprints/-components/submit/preprint-state-machine/action-flow/component.ts new file mode 100644 index 00000000000..559bb60fd33 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/component.ts @@ -0,0 +1,79 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import { waitFor } from '@ember/test-waiters'; + +/** + * The Action Flow Args + */ +interface ActionFlowArgs { + manager: PreprintStateMachine; +} + +/** + * The Action Flow Component + */ +export default class ActionFlow extends Component{ + @service intl!: Intl; + manager = this.args.manager; + + public get isSubmit(): boolean { + return this.manager.isSelected(this.manager.getReviewType); + } + + /** + * Calls the state machine next method + */ + @action + public onPrevious(): void { + this.manager.onPrevious(); + } + + /** + * Calls the state machine next method + */ + @task + @waitFor + public async onNext(): Promise { + await taskFor(this.manager.onNext).perform(); + } + + /** + * Calls the state machine submit method + */ + @task + @waitFor + public async onSubmit(): Promise { + await taskFor(this.manager.onSubmit).perform(); + } + + /** + * Calls the state machine delete method + */ + @task + @waitFor + public async onDelete(): Promise { + await taskFor(this.manager.onDelete).perform(); + } + + /** + * internationalize the delete modal title + */ + public get modalTitle(): string { + return this.intl.t('preprints.submit.action-flow.delete-modal-title', + { singularPreprintWord: this.manager.provider.documentType.singularCapitalized }); + } + + /** + * internationalize the delete modal body + */ + public get modalBody(): string { + return this.intl.t('preprints.submit.action-flow.delete-modal-body', + { singularPreprintWord: this.manager.provider.documentType.singular}); + } + +} diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss b/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss new file mode 100644 index 00000000000..ba43756c244 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss @@ -0,0 +1,59 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.action-flow-container { + width: 100%; + height: 200px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + border-bottom: 1px solid $color-border-gray-darker; + + &.edit { + height: 240px; + } + + .btn { + width: 145px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-evenly; + + + &.white { + color: $color-text-white; + } + + &.disabled { + color: $color-text-black; + background-color: $color-bg-gray-blue-light; + border: 1px solid transparent; + cursor: default; + } + } + + .mobile-disabled { + color: $color-bg-gray-darker; + cursor: default; + } + + .desktop-button-container { + margin-top: 20px; + } + + .mobile-button-container { + width: 33%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + + &.mobile { + height: fit-content; + flex-direction: row; + height: 40px; + border: 0; + } +} 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 new file mode 100644 index 00000000000..4aa4efffafd --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs @@ -0,0 +1,160 @@ +
+ {{#if this.isSubmit}} + {{#if (is-mobile)}} +
+ +
+
+ +
+ {{else}} +
+ +
+ {{/if}} + {{else}} + {{#if (is-mobile)}} +
+ +
+
+ +
+ {{else}} +
+ +
+ {{/if}} + {{/if}} + {{#if @manager.isDeleteButtonDisplayed}} + {{#if (is-mobile)}} +
+ +
+ {{else}} +
+ +
+ {{/if}} + {{/if}} + {{!-- {{#if @manager.isEditFlow}} + {{#if (is-mobile)}} +
+ +
+ {{else}} +
+ +
+ {{/if}} + {{/if}} --}} + {{#if @manager.isWithdrawalButtonDisplayed}} +
+ +
+ {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts new file mode 100644 index 00000000000..8bf0b036b0f --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts @@ -0,0 +1,117 @@ +import Component from '@glimmer/component'; +import { ValidationObject } from 'ember-changeset-validations'; +import { validateLength } from 'ember-changeset-validations/validators'; +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 { 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'; + +const { support: { supportEmail } } = config; + +interface WithdrawalModalArgs { + manager: PreprintStateMachine; +} + +interface WithdrawalFormFields { + withdrawalJustification: string; +} + + +export default class WithdrawalComponent extends Component { + @service intl!: Intl; + @tracked isInvalid = true; + + withdrawalFormValidations: ValidationObject = { + withdrawalJustification: validateLength({ + min: 25, + type: 'greaterThanOrEqualTo', + translationArgs: { + description: this.intl.t('preprints.submit.action-flow.withdrawal-placeholder'), + gte: this.intl.t('preprints.submit.action-flow.withdrawal-input-error'), + }, + }), + }; + + withdrawalFormChangeset = buildChangeset(this.args.manager.preprint, this.withdrawalFormValidations); + + /** + * Calls the state machine delete method + */ + @task + @waitFor + public async onWithdrawal(): Promise { + this.validate(); + if (this.withdrawalFormChangeset.isInvalid) { + return Promise.reject(); + } + this.withdrawalFormChangeset.execute(); + return taskFor(this.args.manager.onWithdrawal).perform(); + } + + @action + public validate(): void { + this.withdrawalFormChangeset.validate(); + this.isInvalid = this.withdrawalFormChangeset.isInvalid; + } + + /** + * internationalize the withdrawal label + */ + public get commentLabel(): string { + return this.intl.t('preprints.submit.action-flow.withdrawal-label'); + } + + /** + * internationalize the modal title + */ + public get modalTitle(): string { + return this.intl.t('preprints.submit.action-flow.withdrawal-modal-title', + { singularPreprintWord: this.args.manager.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 + ) { + return this.intl.t('preprints.submit.action-flow.pre-moderation-notice-pending', + { + singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, + htmlSafe: true, + }) as SafeString; + } else if (this.args.manager.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, + htmlSafe: true, + }) as SafeString; + } else if (this.args.manager.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, + 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, + 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/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss new file mode 100644 index 00000000000..79a49a58c7e --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss @@ -0,0 +1,29 @@ +.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; +} + +.form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .textarea-container { + textarea { + height: 150px; + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs new file mode 100644 index 00000000000..539b9e1f61f --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs @@ -0,0 +1,86 @@ + + + {{#if (is-mobile)}} + + {{else}} + + {{/if}} + + + {{this.modalTitle}} + + +
+ {{this.modalExplanation}} +
+
+ + {{#let (unique-id 'comment') as |commentFieldId|}} + + + {{/let}} + +
+
+ + + + +
\ 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 new file mode 100644 index 00000000000..fc4f74722f8 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -0,0 +1,688 @@ +import Component from '@glimmer/component'; +import PreprintModel, { PreprintDataLinksEnum, PreprintPreregLinksEnum } from 'ember-osf-web/models/preprint'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; +import RouterService from '@ember/routing/router-service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import Intl from 'ember-intl/services/intl'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import FileModel from 'ember-osf-web/models/file'; +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'; + +export enum PreprintStatusTypeEnum { + titleAndAbstract = 'titleAndAbstract', + file = 'file', + metadata = 'metadata', + authorAssertions = 'authorAssertions', + supplements = 'supplements', + review = 'review', +} + +/** + * The State Machine Args + */ +interface StateMachineArgs { + provider: PreprintProviderModel; + preprint: PreprintModel; + setPageDirty: () => void; + resetPageDirty: () => void; +} + +/** + * The Preprint State Machine + */ +export default class PreprintStateMachine extends Component{ + @service store!: Store; + @service router!: RouterService; + @service intl!: Intl; + @service toast!: Toast; + titleAndAbstractValidation = false; + fileValidation = false; + metadataValidation = false; + authorAssertionValidation = false; + supplementValidation = false; + @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; + affiliatedInstitutions = [] as InstitutionModel[]; + + constructor(owner: unknown, args: StateMachineArgs) { + super(owner, args); + + if (this.args.preprint) { + this.preprint = this.args.preprint; + this.setValidationForEditFlow(); + this.isEditFlow = true; + this.isDeleteButtonDisplayed = false; + taskFor(this.canDisplayWitdrawalButton).perform(); + } else { + this.isDeleteButtonDisplayed = true; + this.isWithdrawalButtonDisplayed = false; + this.preprint = this.store.createRecord('preprint', { + provider: this.provider, + }); + } + + 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; + this.metadataValidation = true; + this.authorAssertionValidation = true; + this.supplementValidation = true; + this.isNextButtonDisabled = false; + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onDelete(): Promise { + await this.preprint.deleteRecord(); + 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 + */ + @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 + * + * @description Abstracted method to save after each step + */ + private async saveOnStep(): Promise { + try { + await this.preprint.save(); + this.toast.success( + this.intl.t('preprints.submit.action-flow.success', + { + singularPreprintWord: this.provider.documentType.singular, + }), + ); + } catch (e) { + const errorMessage = this.intl.t('preprints.submit.action-flow.error', + { + singularPreprintWord: this.provider.documentType.singular, + }); + this.toast.error(errorMessage); + captureException(e, { errorMessage }); + } + this.statusFlowIndex++; + this.determinePreviousButtonState(); + } + + /** + * determinePreviousButtonState + * + * @description Abstracted method to determine the state of the previous button + * + * @returns void + */ + private determinePreviousButtonState(): void { + this.isPreviousButtonDisabled = this.statusFlowIndex === 1; + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onSubmit(): Promise { + this.args.resetPageDirty(); + if (!this.isEditFlow) { + if (this.provider.reviewsWorkflow) { + const reviewAction = this.store.createRecord('review-action', { + actionTrigger: 'submit', + target: this.preprint, + }); + await reviewAction.save(); + } else { + this.preprint.isPublished = true; + await this.preprint.save(); + } + + } + + await this.preprint.reload(); + + await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onNext(): Promise { + if (this.isEditFlow) { + this.args.resetPageDirty(); + } else { + this.args.setPageDirty(); + } + this.isNextButtonDisabled = true; + + if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.titleAndAbstract) && + this.titleAndAbstractValidation + ) { + await this.saveOnStep(); + await this.preprint.files; + this.isNextButtonDisabled = !this.fileValidation; + return; + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && + this.fileValidation + ) { + await this.saveOnStep(); + this.isNextButtonDisabled = !this.metadataValidation; + return; + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && + 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 { + this.isNextButtonDisabled = !this.supplementValidation; + } + return; + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.authorAssertions) && + this.authorAssertionValidation + ) { + await this.saveOnStep(); + this.isNextButtonDisabled = !this.supplementValidation; + return; + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.supplements) && + this.supplementValidation + ) { + await this.saveOnStep(); + return; + } + } + + private setPageDirty(): void { + if (this.isEditFlow) { + this.args.setPageDirty(); + } + } + + /** + * Callback for the action-flow component + */ + @action + public validateTitleAndAbstract(valid: boolean): void { + this.titleAndAbstractValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateFile(valid: boolean): void { + this.fileValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateMetadata(valid: boolean): void { + this.metadataValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateAuthorAssertions(valid: boolean): void { + if (this.preprint.hasCoi === false) { + this.preprint.conflictOfInterestStatement = null; + } + if (this.preprint.hasDataLinks === PreprintDataLinksEnum.NOT_APPLICABLE) { + this.preprint.whyNoData = null; + } + if (this.preprint.hasPreregLinks === PreprintPreregLinksEnum.NOT_APPLICABLE) { + this.preprint.whyNoPrereg = null; + } + this.authorAssertionValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateSupplements(valid: boolean): void { + this.supplementValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + @action + public onPrevious(): void { + if (this.statusFlowIndex > 1) { + this.statusFlowIndex--; + } + this.determinePreviousButtonState(); + this.isNextButtonDisabled = false; + } + + @action + public onClickStep(type: string): void { + this.isNextButtonDisabled = !this.isFinished(type); + if ( + type === PreprintStatusTypeEnum.titleAndAbstract && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.file && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.metadata && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.authorAssertions && + this.statusFlowIndex > this.getTypeIndex(type) && + this.displayAuthorAssertions + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.supplements && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.review && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } + + this.determinePreviousButtonState(); + } + + @action + public isSelected(type: string): boolean { + if ( + type === PreprintStatusTypeEnum.titleAndAbstract && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.file && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.metadata && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.authorAssertions && + this.getTypeIndex(type) === this.statusFlowIndex && + this.displayAuthorAssertions + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.supplements && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.review && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else { + return false; + } + } + + @action + public getAnalytics(type: string): string { + return this.intl.t('preprints.submit.data-analytics', {statusType: this.getStatusTitle(type) } ); + } + + + @action + public isDisabled(type: string): boolean { + if ( + type === PreprintStatusTypeEnum.titleAndAbstract && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.file && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.metadata && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.authorAssertions && + this.getTypeIndex(type) === this.statusFlowIndex && + this.displayAuthorAssertions + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.supplements && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.review && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else { + return false; + } + } + + 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; + } else { + return 0; + } + } + + @action + public isFinished(type: string): boolean { + if (this.displayAuthorAssertions && this.statusFlowIndex > this.getTypeIndex(type)) { + return true; + } else if (!this.displayAuthorAssertions && this.statusFlowIndex > this.getTypeIndex(type)) { + return true; + } else if (this.statusFlowIndex > this.getTypeIndex(type)) { + return true; + } else { + return false; + } + } + + @action + public getStatusTitle(type: string): string { + switch (type) { + case PreprintStatusTypeEnum.titleAndAbstract: + return this.intl.t('preprints.submit.status-flow.step-title-and-abstract'); + case PreprintStatusTypeEnum.file: + return this.intl.t('preprints.submit.status-flow.step-file'); + case PreprintStatusTypeEnum.metadata: + return this.intl.t('preprints.submit.status-flow.step-metadata'); + case PreprintStatusTypeEnum.authorAssertions: + return this.intl.t('preprints.submit.status-flow.step-author-assertions'); + case PreprintStatusTypeEnum.supplements: + return this.intl.t('preprints.submit.status-flow.step-supplements'); + case PreprintStatusTypeEnum.review: + return this.intl.t('preprints.submit.status-flow.step-review'); + default: + return ''; + } + } + + @action + public getFaIcon(type: string): string { + if (this.isSelected(type)) { + return 'dot-circle'; + } else if (this.isFinished(type)) { + return 'check-circle'; + } else { + return 'circle'; + } + } + + /** + * shoulddisplayStatusType + * + * @description Determines if the status type should be displayed + * + * @returns boolean + */ + public shouldDisplayStatusType(type: string): boolean{ + return type === PreprintStatusTypeEnum.authorAssertions ? this.displayAuthorAssertions : true; + } + + /** + * getTitleAndAbstractType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getTitleAndAbstractType(): string { + return PreprintStatusTypeEnum.titleAndAbstract; + } + + /** + * getFileType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getFileType(): string { + return PreprintStatusTypeEnum.file; + } + + /** + * getMetadataType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getMetadataType(): string { + return PreprintStatusTypeEnum.metadata; + } + + /** + * getAuthorAssertionsType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getAuthorAssertionsType(): string { + return PreprintStatusTypeEnum.authorAssertions; + } + + /** + * getSupplementsType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getSupplementsType(): string { + return PreprintStatusTypeEnum.supplements; + } + + /** + * getReviewType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getReviewType(): string { + return PreprintStatusTypeEnum.review; + } + + @task + @waitFor + public async addProjectFile(file: FileModel): Promise{ + await file.copy(this.preprint, '/', 'osfstorage', { + conflict: 'replace', + }); + const theFiles = await this.preprint.files; + const rootFolder = await theFiles.firstObject!.rootFolder; + 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/status-flow/status-flow-display/component.ts b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/component.ts new file mode 100644 index 00000000000..b7599719046 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/component.ts @@ -0,0 +1,69 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; + +/** + * The Status Flow Display Args + */ +interface StatusFlowDisplayArgs { + manager: PreprintStateMachine; + isDisplayMobileMenu: boolean; + leftNavToggle: () => void; + type: string; +} + +export default class StatusFlowDisplay extends Component{ + + type = this.args.type; + + private get manager(): PreprintStateMachine { + return this.args.manager; + } + + public get shouldDisplayStatusType(): boolean { + let isDisplay = this.manager.shouldDisplayStatusType(this.type); + if (this.args.isDisplayMobileMenu) { + isDisplay &&= this.isSelected; + } + + return isDisplay; + } + + public get getStatusTitle(): string { + return this.manager.getStatusTitle(this.type); + } + + public get isSelected(): boolean { + return this.manager.isSelected(this.type); + } + + public get isFinished(): boolean { + return this.manager.isFinished(this.type); + } + + public get isDisabled(): boolean { + return this.manager.isDisabled(this.type); + } + + public get getAnalytics(): string { + return this.manager.getAnalytics(this.type); + } + + public get getFaIcon(): string { + return this.args.manager.getFaIcon(this.type); + } + + public onClick(): void { + if (!this.args.isDisplayMobileMenu) { + this.args.leftNavToggle(); + } + this.args.manager.onClickStep(this.type); + } + + public get getLinkClass(): string { + if (this.isSelected) { + return 'selected'; + } else { + return 'unfinished'; + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss new file mode 100644 index 00000000000..b203917bbaa --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss @@ -0,0 +1,74 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.status-container { + width: 192px; + height: 40px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + background-color: inherit; + z-index: 1; + + + &.selected { + width: 193px; + margin-right: -1px; + border: 1px solid $color-border-gray-darker; + border-right: 0; + background-color: $color-bg-white; + } + + .graphics-container { + width: 25px; + height: 25px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: $color-bg-white; + + .dot-circle { + color: $color-bg-black; + } + + .check-circle { + color: $brand-success; + } + + .circle { + color: $color-bg-gray; + }; + } + + .link-container { + padding-left: 10px; + width: 100%; + height: 25px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + .btn { + font-weight: bold; + } + + &.cursor { + cursor: pointer; + } + + + .finished { + color: $brand-success; + } + } + + &.mobile { + width: 100%; + + &.selected { + border: 0; + } + } +} 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 new file mode 100644 index 00000000000..8ad088e1e29 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs @@ -0,0 +1,44 @@ +{{#if this.shouldDisplayStatusType}} +
+
+ {{#if @isDisplayMobileMenu}} + + {{else}} + + {{/if}} +
+
+ {{#if this.isFinished}} + + {{else}} +
+ {{ this.getStatusTitle }} +
+ {{/if}} +
+
+{{/if}} \ No newline at end of file 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 new file mode 100644 index 00000000000..ed405d15eec --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss @@ -0,0 +1,37 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.status-flow-container { + width: 205px; + padding-top: 10px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-start; + + .line { + position: absolute; + border-left: 3px solid $color-border-gray; + width: 0; + top: 20px; + left: 22.5px; + z-index: 0; + height: 140px; + + &.long { + height: 175px; + } + } + + &.mobile { + padding-top: 0; + width: 100%; + + .line { + display: none; + + &.long { + 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 new file mode 100644 index 00000000000..0633acae74f --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs @@ -0,0 +1,45 @@ +
+
+ + + + + + +
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/styles.scss b/app/preprints/-components/submit/preprint-state-machine/styles.scss new file mode 100644 index 00000000000..ca382e9984e --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/styles.scss @@ -0,0 +1,35 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +$container-width: 1144px; +$side-container-width: 205px; +$middle-container-width: $container-width - $side-container-width - $side-container-width; +$page-height: 1000px; + +.preprint-state-machine-container { + height: $page-height; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-size: 16px; + + .flow-status-container { + height: $page-height; + width: $side-container-width; + } + + .flow-input-container { + height: $page-height; + width: $middle-container-width; + } + + .flow-action-container { + height: $page-height; + width: $side-container-width; + } + + &.mobile { + height: fit-content; + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/template.hbs b/app/preprints/-components/submit/preprint-state-machine/template.hbs new file mode 100644 index 00000000000..39b51bbad68 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/template.hbs @@ -0,0 +1,49 @@ +{{yield (hash + onDelete=this.onDelete + onWithdrawal=this.onWithdrawal + onClickStep=this.onClickStep + addProjectFile=this.addProjectFile + onNext=this.onNext + onPrevious=this.onPrevious + onSubmit=this.onSubmit + onCancel=this.onCancel + preprint=this.preprint + provider=this.provider + isNextButtonDisabled=this.isNextButtonDisabled + isPreviousButtonDisabled=this.isPreviousButtonDisabled + isEditFlow=this.isEditFlow + isDeleteButtonDisplayed=this.isDeleteButtonDisplayed + isWithdrawalButtonDisplayed=this.isWithdrawalButtonDisplayed + + getTitleAndAbstractType=this.getTitleAndAbstractType + getFileType=this.getFileType + getMetadataType=this.getMetadataType + getAuthorAssertionsType=this.getAuthorAssertionsType + getSupplementsType=this.getSupplementsType + getReviewType=this.getReviewType + + validateTitleAndAbstract=this.validateTitleAndAbstract + validateFile=this.validateFile + validateMetadata=this.validateMetadata + validateAuthorAssertions=this.validateAuthorAssertions + validateSupplements=this.validateSupplements + + shouldDisplayStatusType=this.shouldDisplayStatusType + getStatusTitle=this.getStatusTitle + isSelected=this.isSelected + isFinished=this.isFinished + isDisabled=this.isDisabled + onClick=this.onClick + getAnalytics=this.getAnalytics + getFaIcon=this.getFaIcon + + 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/component.ts b/app/preprints/-components/submit/review/component.ts new file mode 100644 index 00000000000..5eaa7313fbf --- /dev/null +++ b/app/preprints/-components/submit/review/component.ts @@ -0,0 +1,91 @@ +import Store from '@ember-data/store'; +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import moment from 'moment-timezone'; +import Intl from 'ember-intl/services/intl'; +import { PreprintProviderReviewsWorkFlow } from 'ember-osf-web/models/provider'; +import { SafeString } from '@ember/template/-private/handlebars'; + +/** + * The Review Args + */ +interface ReviewArgs { + manager: PreprintStateMachine; +} + +/** + * The Review Component + */ +export default class Review extends Component{ + @service store!: Store; + @tracked preprint = this.args.manager.preprint; + @tracked provider?: any; + @tracked license?: any; + @tracked contributors?: any; + @tracked subjects?: any; + @service intl!: Intl; + @tracked displayProviderAgreement = false; + @tracked providerAgreement: string | SafeString = ''; + + constructor(owner: unknown, args: ReviewArgs) { + super(owner, args); + + taskFor(this.loadPreprint).perform(); + } + + @task + @waitFor + private async loadPreprint() { + this.provider = await this.preprint.provider.content; + if ( + this.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION || + this.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.POST_MODERATION + ) { + this.displayProviderAgreement = true; + + const moderationType = this.provider.reviewsWorkflow === + PreprintProviderReviewsWorkFlow.PRE_MODERATION ? + PreprintProviderReviewsWorkFlow.PRE_MODERATION : + PreprintProviderReviewsWorkFlow.POST_MODERATION; + this.providerAgreement = this.intl.t('preprints.submit.step-review.agreement-provider', + { + providerName : this.provider.name, + moderationType, + htmlSafe: true, + }); + } + + this.license = this.preprint.license; + this.subjects = await this.preprint.queryHasMany('subjects'); + } + + public get providerLogo(): string | undefined { + return this.provider.get('assets')?.square_color_no_transparent; + } + + public get displayPublicationDoi(): string { + return this.preprint.articleDoiUrl || this.intl.t('general.not-applicable'); + } + + public get displayPublicationDate(): string { + return this.preprint.originalPublicationDate + ? moment(this.preprint.originalPublicationDate).format('YYYY-MM-DD') + : this.intl.t('general.not-applicable'); + } + + public get displayPublicationCitation(): string { + return this.preprint.customPublicationCitation + ? this.preprint.customPublicationCitation + : this.intl.t('general.not-applicable'); + } + + public get providerServiceLabel(): string { + return this.intl.t('preprints.submit.step-review.preprint-service', + { singularPreprintWord: this.provider.documentType.singularCapitalized }); + } +} diff --git a/app/preprints/-components/submit/review/styles.scss b/app/preprints/-components/submit/review/styles.scss new file mode 100644 index 00000000000..0acb46ee04d --- /dev/null +++ b/app/preprints/-components/submit/review/styles.scss @@ -0,0 +1,67 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + + .step-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + } + + .content-container { + width: 100%; + margin-top: 20px; + + h4 { + margin-top: 10px; + margin-bottom: 10px; + font-weight: bold; + } + + .display { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + &.ellipsis { + text-overflow: ellipsis; + overflow: hidden; + white-space: normal; + } + + .image { + width: 30px; + height: 30px; + margin-right: 10px; + } + + .text { + height: 30px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + } + } + } + + hr { + margin-top: 20px; + margin-bottom: 20px; + width: 100%; + border: 1px solid $color-border-gray; + } + } + + &.mobile { + height: fit-content; + } +} diff --git a/app/preprints/-components/submit/review/template.hbs b/app/preprints/-components/submit/review/template.hbs new file mode 100644 index 00000000000..88af48a6982 --- /dev/null +++ b/app/preprints/-components/submit/review/template.hbs @@ -0,0 +1,161 @@ +
+ {{#if this.loadPreprint.isRunning}} + + {{else}} +
+

+ {{t 'preprints.submit.step-review.agreement-title'}} +

+ +
+ {{t 'preprints.submit.step-review.agreement-user'}} +
+ {{#if this.displayProviderAgreement}} +
+ {{this.providerAgreement}} +
+
+ {{t 'preprints.submit.step-review.agreement-provider-two' htmlSafe=true}} +
+ {{/if}} +
+
+
+

+ {{t 'preprints.submit.step-title.title'}} +

+ +
+
+ {{ this.providerServiceLabel}} +
+
+ {{t +
+ {{this.provider.name}} +
+
+
+
+

+ {{t 'preprints.submit.step-review.preprint-title'}} +

+
+ {{this.preprint.title}} +
+
+
+ +
+
+
+
+

+ {{t 'preprints.submit.step-metadata.title'}} +

+
+
+ {{t 'preprints.submit.step-review.contributors'}} +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+

+ {{t 'preprints.submit.step-review.publication-doi'}} +

+
+ {{this.displayPublicationDoi}} +
+
+
+
+

+ {{t 'preprints.submit.step-review.publication-date'}} +

+
+ {{this.displayPublicationDate}} +
+
+
+

+ {{t 'preprints.submit.step-review.publication-citation'}} +

+
+ {{this.displayPublicationCitation}} +
+
+
+
+
+

+ {{t 'preprints.submit.step-assertions.title'}} +

+
+ +
+
+ +
+
+ +
+
+
+
+

+ {{t 'preprints.submit.step-supplements.title'}} +

+
+ +
+
+
+ {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/styles.scss b/app/preprints/-components/submit/styles.scss new file mode 100644 index 00000000000..639798d6426 --- /dev/null +++ b/app/preprints/-components/submit/styles.scss @@ -0,0 +1,33 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + + +.preprint-state-machine-container { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-size: 16px; + + + .spinner-container { + z-index: 1; + position: absolute; + top: 0; + left: 205px; + bottom: 0; + right: 190px; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 200px; + background-color: $color-bg-white-transparent; + } + + &.mobile { + .spinner-container { + left: 0; + right: 0; + } + } +} diff --git a/app/preprints/-components/submit/submission-flow/styles.scss b/app/preprints/-components/submit/submission-flow/styles.scss new file mode 100644 index 00000000000..5bf5765efdc --- /dev/null +++ b/app/preprints/-components/submit/submission-flow/styles.scss @@ -0,0 +1,63 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.submit-page-container { + background-color: $color-bg-gray-lighter; +} + +.header-container { + padding: 30px 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: $color-text-white; + background: url('assets/images/preprints/preprints-detail-header-overlay.png') top center $color-bg-color-grey; + + .header { + max-width: 1140px; + width: 100%; + font-size: 48px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + + &.mobile { + margin-left: 10px; + width: calc(100% - 10px); + } + } +} + +.top-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + + .top-left, + .top-right { + width: calc(50% - 10px); + margin-right: 10px; + } + + .top-right { + border-left: 1px solid $color-border-gray; + } +} + +.main-container { + background-color: $color-bg-white; + padding: 20px; + width: 100%; + flex-direction: column; + display: flex; + justify-content: flex-start; + align-items: flex-start; + + &.mobile { + padding: 10px; + padding-top: 0; + } +} diff --git a/app/preprints/-components/submit/submission-flow/template.hbs b/app/preprints/-components/submit/submission-flow/template.hbs new file mode 100644 index 00000000000..4fb6ff2960b --- /dev/null +++ b/app/preprints/-components/submit/submission-flow/template.hbs @@ -0,0 +1,59 @@ +{{page-title (t @header documentType=this.provider.documentType)}} + + + + +
+ {{t @header + documentType = @provider.documentType.singularCapitalized + }} +
+
+ {{#if (is-mobile)}} + +
+
+ +
+
+ +
+ +
+
+ {{/if}} + + + + + + + {{#if (not (is-mobile))}} + + + + {{/if}} +
+
+ diff --git a/app/preprints/-components/submit/supplements/component.ts b/app/preprints/-components/submit/supplements/component.ts new file mode 100644 index 00000000000..772aac50048 --- /dev/null +++ b/app/preprints/-components/submit/supplements/component.ts @@ -0,0 +1,84 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import { taskFor } from 'ember-concurrency-ts'; +import NodeModel from 'ember-osf-web/models/node'; + +/** + * The Supplements Args + */ +interface SupplementsArgs { + manager: PreprintStateMachine; +} + +/** + * The Supplements Component + */ +export default class Supplements extends Component{ + @tracked displayExistingNodeWidget = false; + @tracked isSupplementAttached = false; + @tracked isModalOpen = false; + + constructor(owner: unknown, args: SupplementsArgs) { + super(owner, args); + + if(this.args.manager.preprint.get('node')?.get('id')) { + this.isSupplementAttached = true; + } + + this.args.manager.validateSupplements(true); + } + + public get isDisplayCancelButton(): boolean { + return this.displayExistingNodeWidget; + } + + @action + public onCancelProjectAction(): void { + this.displayExistingNodeWidget = false; + this.isModalOpen = false; + } + + @action + public onConnectOsfProject(): void { + this.displayExistingNodeWidget = true; + } + + @action + public onCreateOsfProject(): void { + this.displayExistingNodeWidget = false; + this.isModalOpen = true; + } + + @task + @waitFor + private async saveSelectedProject(): Promise { + await this.args.manager.preprint.save(); + this.validate(); + } + + @task + @waitFor + public async removeSelectedProject(): Promise { + await this.args.manager.preprint.removeM2MRelationship('node'); + await this.args.manager.preprint.reload(); + this.isSupplementAttached = false; + this.validate(); + } + + @action + public projectSelected(node: NodeModel): void { + this.args.manager.preprint.set('node', node); + taskFor(this.saveSelectedProject).perform(); + this.isSupplementAttached = true; + this.onCancelProjectAction(); + } + + @action + public validate(): void { + this.args.manager.validateSupplements(true); + } +} diff --git a/app/preprints/-components/submit/supplements/styles.scss b/app/preprints/-components/submit/supplements/styles.scss new file mode 100644 index 00000000000..43a19b64fe1 --- /dev/null +++ b/app/preprints/-components/submit/supplements/styles.scss @@ -0,0 +1,71 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + margin-bottom: 20px; + } + + .supplement-container { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: center; + + .supplement { + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + + .button-container { + margin-top: 10px; + margin-bottom: 10px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + + .btn-width { + width: calc(50% - 20px); + + &.selected { + background-color: $secondary-blue; + color: $color-text-white; + } + } + + &.mobile { + flex-direction: column; + + .btn-width { + width: 100%; + margin-bottom: 10px; + } + } + } + + .cancel-button-container { + margin-top: 10px; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + } + + + &.mobile { + height: fit-content; + } +} diff --git a/app/preprints/-components/submit/supplements/template.hbs b/app/preprints/-components/submit/supplements/template.hbs new file mode 100644 index 00000000000..5b9aab66edc --- /dev/null +++ b/app/preprints/-components/submit/supplements/template.hbs @@ -0,0 +1,72 @@ +
+

+ {{t 'preprints.submit.step-supplements.title'}} +

+ {{#unless this.isSupplementAttached}} +

+ {{t 'preprints.submit.step-supplements.description'}} +

+ {{/unless}} + + {{#if this.isSupplementAttached}} +
+
+ +
+
+ {{else}} +
+ + + +
+ + {{#if this.displayExistingNodeWidget }} + + +
+ +
+ {{/if}} + + + {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/template.hbs b/app/preprints/-components/submit/template.hbs new file mode 100644 index 00000000000..6c833fe0a34 --- /dev/null +++ b/app/preprints/-components/submit/template.hbs @@ -0,0 +1,37 @@ +
+ {{#if (or @manager.onNext.isRunning @manager.onSubmit.isRunning @manager.addProjectFile.isRunning @manager.onWithdrawal.isRunning)}} +
+ +
+ {{/if}} + {{#if this.isTitleAndAbstractActive}} + + {{/if}} + {{#if this.isFileActive}} + + {{/if}} + {{#if this.isMetadataActive}} + + {{/if}} + {{#if this.isAuthorAssertionsActive}} + + {{/if}} + {{#if this.isSupplementsActive}} + + {{/if}} + {{#if this.isReviewActive}} + + {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/title-and-abstract/component.ts b/app/preprints/-components/submit/title-and-abstract/component.ts new file mode 100644 index 00000000000..07726a12158 --- /dev/null +++ b/app/preprints/-components/submit/title-and-abstract/component.ts @@ -0,0 +1,62 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { action } from '@ember/object'; +import { ValidationObject } from 'ember-changeset-validations'; +import { validatePresence, validateLength } from 'ember-changeset-validations/validators'; +import buildChangeset from 'ember-osf-web/utils/build-changeset'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +/** + * The TitleAndAbstract Args + */ +interface TitleAndAbstractArgs { + manager: PreprintStateMachine; +} + +interface TitleAndAbstractForm { + title: string; + description: string; +} + +/** + * The Title And Abstract Component + */ +export default class TitleAndAbstract extends Component{ + @service intl!: Intl; + titleAndAbstractFormValidation: ValidationObject = { + title: validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + description: [ + validatePresence({ + presence: true, + ignoreBlank: true, + type: 'empty', + }), + validateLength({ + min: 20, + type: 'greaterThanOrEqualTo', + translationArgs: { + description: this.intl.t('preprints.submit.step-title.abstract-input'), + gte: this.intl.t('preprints.submit.step-title.abstract-input-error'), + }, + }), + ], + }; + + titleAndAbstractFormChangeset = buildChangeset(this.args.manager.preprint, this.titleAndAbstractFormValidation); + + @action + public validate(): void { + this.titleAndAbstractFormChangeset.validate(); + if (this.titleAndAbstractFormChangeset.isInvalid) { + this.args.manager.validateTitleAndAbstract(false); + return; + } + this.titleAndAbstractFormChangeset.execute(); + this.args.manager.validateTitleAndAbstract(true); + } +} diff --git a/app/preprints/-components/submit/title-and-abstract/styles.scss b/app/preprints/-components/submit/title-and-abstract/styles.scss new file mode 100644 index 00000000000..bf2bbf13a5a --- /dev/null +++ b/app/preprints/-components/submit/title-and-abstract/styles.scss @@ -0,0 +1,33 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + margin-bottom: 20px; + } + + .form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .input-container { + margin-bottom: 20px; + } + + .textarea-container { + textarea { + height: 150px; + } + } + } +} + diff --git a/app/preprints/-components/submit/title-and-abstract/template.hbs b/app/preprints/-components/submit/title-and-abstract/template.hbs new file mode 100644 index 00000000000..a53255e1aa2 --- /dev/null +++ b/app/preprints/-components/submit/title-and-abstract/template.hbs @@ -0,0 +1,53 @@ +
+ +

+ {{t 'preprints.submit.step-title.title'}} +

+
+ + {{#let (unique-id 'title') as |titleFieldId|}} + + + {{/let}} + {{#let (unique-id 'abstract') as |abstractFieldId|}} + + + {{/let}} + +
+
\ No newline at end of file diff --git a/app/preprints/detail/controller.ts b/app/preprints/detail/controller.ts index eb31f7f8c47..63d3e8c19e1 100644 --- a/app/preprints/detail/controller.ts +++ b/app/preprints/detail/controller.ts @@ -95,6 +95,12 @@ export default class PrePrintsDetailController extends Controller { return (this.model.preprint.currentUserPermissions).includes(Permission.Admin); } + private hasReadWriteAccess(): boolean { + // True if the current user has write permissions for the node that contains the preprint + return (this.model.preprint.currentUserPermissions.includes(Permission.Write)); + } + + get userIsContrib(): boolean { if (this.isAdmin()) { return true; @@ -103,7 +109,8 @@ export default class PrePrintsDetailController extends Controller { this.model.contributors.forEach((author: ContributorModel) => { authorIds.push(author.id); }); - return this.currentUser.currentUserId ? authorIds.includes(this.currentUser.currentUserId) : false; + const authorId = `${this.model.preprint.id}-${this.currentUser.currentUserId}`; + return this.currentUser.currentUserId ? authorIds.includes(authorId) && this.hasReadWriteAccess() : false; } return false; } diff --git a/app/preprints/detail/template.hbs b/app/preprints/detail/template.hbs index c0d24703e5b..a6a14847d5f 100644 --- a/app/preprints/detail/template.hbs +++ b/app/preprints/detail/template.hbs @@ -1,6 +1,5 @@ {{page-title this.displayTitle replace=false}} -
{{this.editButtonLabel}} @@ -124,11 +124,11 @@
- {{t 'preprints.detail.share.views'}}: + {{t 'preprints.detail.share.views'}}: - {{this.model.preprint.apiMeta.metrics.views}} | + {{this.model.preprint.apiMeta.metrics.views}} | - {{t 'preprints.detail.share.downloads'}}: + {{t 'preprints.detail.share.downloads'}}: {{this.model.preprint.apiMeta.metrics.downloads}} @@ -166,6 +166,8 @@
+ + {{#if this.model.preprint.node.links}}

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

@@ -192,6 +194,12 @@
{{/if}} + {{#if this.model.preprint.customPublicationCitation}} +
+

{{t 'preprints.detail.publication-citation'}}

+ {{this.model.preprint.customPublicationCitation}} +
+ {{/if}} diff --git a/app/preprints/edit/controller.ts b/app/preprints/edit/controller.ts new file mode 100644 index 00000000000..3c41eb6e790 --- /dev/null +++ b/app/preprints/edit/controller.ts @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { action} from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class PreprintEdit extends Controller { + @tracked isPageDirty = false; + + @action + setPageDirty() { + this.isPageDirty = true; + } + + @action + resetPageDirty() { + this.isPageDirty = false; + } +} diff --git a/app/preprints/edit/route.ts b/app/preprints/edit/route.ts new file mode 100644 index 00000000000..4156ead0d3a --- /dev/null +++ b/app/preprints/edit/route.ts @@ -0,0 +1,101 @@ +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'; +// eslint-disable-next-line ember/no-mixins +import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import MetaTags, { HeadTagDef } from 'ember-osf-web/services/meta-tags'; +import Theme from 'ember-osf-web/services/theme'; +import requireAuth from 'ember-osf-web/decorators/require-auth'; +import { action, computed } from '@ember/object'; +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, {}) { + @service store!: Store; + @service theme!: Theme; + @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 + confirmationMessage = this.intl.t('preprints.submit.action-flow.save-before-exit'); + + 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 = await this.store.findRecord('preprint', args.guid); + + if ( + !preprint.currentUserPermissions.includes(Permission.Write) || + preprint.isWithdrawn + ) { + 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, + brand: provider.brand.content, + }; + } catch (e) { + this.router.transitionTo('not-found', `preprints/${args.provider_id}`); + return null; + } + } + + afterModel(model: PreprintProviderModel) { + if (model && model.assets && model.assets.favicon) { + const headTags = [{ + type: 'link', + attrs: { + rel: 'icon', + href: model.assets.favicon, + }, + }]; + this.set('headTags', headTags); + } + } + + // This tells ember-onbeforeunload's ConfirmationMixin whether or not to stop transitions + // This is for when the user leaves the site or does a full app reload + @computed('controller.isPageDirty') + get isPageDirty() { + const controller = this.controller as PreprintEdit; + return () => controller.isPageDirty; + } + + // This is for when the user leaves the page via the router + @action + willTransition(transition: Transition) { + const controller = this.controller as PreprintEdit; + if (controller.isPageDirty) { + if (!window.confirm(this.intl.t('preprints.submit.action-flow.save-before-exit'))) { + transition.abort(); + } + } + } +} diff --git a/app/preprints/edit/template.hbs b/app/preprints/edit/template.hbs new file mode 100644 index 00000000000..74f4681d55c --- /dev/null +++ b/app/preprints/edit/template.hbs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/app/preprints/index/controller.ts b/app/preprints/index/controller.ts index b7dc6e2c6e8..59f55cfa839 100644 --- a/app/preprints/index/controller.ts +++ b/app/preprints/index/controller.ts @@ -4,22 +4,17 @@ import { action } from '@ember/object'; import RouterService from '@ember/routing/router-service'; import { inject as service } from '@ember/service'; import Theme from 'ember-osf-web/services/theme'; -import Media from 'ember-responsive'; import Intl from 'ember-intl/services/intl'; +import config from 'ember-osf-web/config/environment'; export default class Preprints extends Controller { @service store!: Store; @service theme!: Theme; @service router!: RouterService; - @service media!: Media; @service intl!: Intl; - get isMobile(): boolean { - return this.media.isMobile; - } - - get isOsf(): boolean { - return this.theme?.provider?.id === 'osf'; + get isDefaultProvider(): boolean { + return this.theme?.provider?.id === config.defaultProvider; } @action diff --git a/app/preprints/index/template.hbs b/app/preprints/index/template.hbs index 216f45a12ee..514fa792490 100644 --- a/app/preprints/index/template.hbs +++ b/app/preprints/index/template.hbs @@ -1,4 +1,4 @@ -
@@ -155,8 +155,8 @@ {{!ADVISORY GROUP}} {{#if this.theme.provider.advisoryBoard.length}}
{{html-safe this.theme.provider.advisoryBoard}} diff --git a/app/preprints/my-preprints/route.ts b/app/preprints/my-preprints/route.ts new file mode 100644 index 00000000000..d0e65030951 --- /dev/null +++ b/app/preprints/my-preprints/route.ts @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; +import requireAuth from 'ember-osf-web/decorators/require-auth'; +import { inject as service } from '@ember/service'; +import Store from '@ember-data/store'; +import CurrentUser from 'ember-osf-web/services/current-user'; + +@requireAuth() +export default class PreprintsMyPreprintsRoute extends Route { + @service store!: Store; + @service currentUser!: CurrentUser; + + async model() { + return this.currentUser.user; + } +} diff --git a/app/preprints/my-preprints/styles.scss b/app/preprints/my-preprints/styles.scss new file mode 100644 index 00000000000..fae932d93d4 --- /dev/null +++ b/app/preprints/my-preprints/styles.scss @@ -0,0 +1,69 @@ +// stylelint-disable declaration-property-value-blacklist +// stylelint-disable selector-no-qualifying-type + +.ContentBackground { + display: flex; + flex-grow: 1; + z-index: 1; + justify-content: center; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url('images/preprints/bg-light.jpg'); + background-repeat: no-repeat; + background-size: cover; + filter: grayscale(1); + } + + :global(.list-group-item) { + margin-bottom: 20px; + } + + :global(.media-desktop), + :global(.media-tablet), + :global(.media-mobile) { + margin-top: -38px; + } + +} + +.Hero { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 100%; + min-height: 100%; + position: relative; + background: url('images/default-brand/bg-dark.jpg'); + background-size: cover; + + .Title { + min-height: 150px; + + h1 { + color: $color-text-white; + font-weight: 400; + margin-top: 0; + margin-bottom: 0; + padding: 1em 2em; + } + } +} + +.GutterBody { + padding-top: 85px; + padding-bottom: 85px; +} + +.SortDescription { + text-align: right; + margin-top: 10px; + margin-right: 15px; +} diff --git a/app/preprints/my-preprints/template.hbs b/app/preprints/my-preprints/template.hbs new file mode 100644 index 00000000000..31a14ed7971 --- /dev/null +++ b/app/preprints/my-preprints/template.hbs @@ -0,0 +1,41 @@ +{{page-title (t 'preprints.my_preprints.header')}} + + + +
+

+ {{t 'preprints.my_preprints.header'}} +

+
+
+ +
+
+ {{t 'preprints.my_preprints.sorted'}} +
+ + + + {{#if preprint}} + + {{else}} + {{placeholder.text lines=1}} + {{/if}} + + + +
+

{{t 'preprints.noPreprints'}}

+
+
+
+
+
+
+
diff --git a/app/preprints/select/route.ts b/app/preprints/select/route.ts new file mode 100644 index 00000000000..dc214fea5a9 --- /dev/null +++ b/app/preprints/select/route.ts @@ -0,0 +1,27 @@ +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import Store from '@ember-data/store'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import requireAuth from 'ember-osf-web/decorators/require-auth'; +import Theme from 'ember-osf-web/services/theme'; +import config from 'ember-osf-web/config/environment'; + +@requireAuth() +export default class PreprintSelectRoute extends Route { + @service store!: Store; + @service theme!: Theme; + + async model() { + const submissionProviders: PreprintProviderModel[] = await this.store.query('preprint-provider', { + filter: { + allow_submissions: true, + }, + }); + + this.theme.set('id', config.defaultProvider); + + return { + submissionProviders, + }; + } +} diff --git a/app/preprints/select/styles.scss b/app/preprints/select/styles.scss new file mode 100644 index 00000000000..083f8f071e6 --- /dev/null +++ b/app/preprints/select/styles.scss @@ -0,0 +1,43 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +@import 'app/styles/layout'; + +.select-page-container { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .header-container { + padding: 30px 0; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + background: url('assets/images/default-brand/bg-dark.jpg') top center $color-bg-color-grey; + + .header-clamp-width-container { + @include clamp-width; + + .header { + margin: 5px 10px; + font-size: 48px; + color: $color-text-white; + } + } + } + + &.mobile { + .header-container { + text-align: center; + + .header-clamp-width-container { + .header { + font-size: 36px; + } + } + } + } +} diff --git a/app/preprints/select/template.hbs b/app/preprints/select/template.hbs new file mode 100644 index 00000000000..def36ced049 --- /dev/null +++ b/app/preprints/select/template.hbs @@ -0,0 +1,15 @@ +{{page-title (t 'preprints.select.page-title')}} + +
+
+
+

+ {{t 'preprints.select.title'}} +

+
+
+ +
diff --git a/app/preprints/submit/controller.ts b/app/preprints/submit/controller.ts new file mode 100644 index 00000000000..729bb2a2b6b --- /dev/null +++ b/app/preprints/submit/controller.ts @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { action} from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class PreprintSubmit extends Controller { + @tracked isPageDirty = false; + + @action + setPageDirty() { + this.isPageDirty = true; + } + + @action + resetPageDirty() { + this.isPageDirty = false; + } +} diff --git a/app/preprints/submit/route.ts b/app/preprints/submit/route.ts new file mode 100644 index 00000000000..feeeac02215 --- /dev/null +++ b/app/preprints/submit/route.ts @@ -0,0 +1,84 @@ +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 PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import MetaTags, { HeadTagDef } from 'ember-osf-web/services/meta-tags'; +import Theme from 'ember-osf-web/services/theme'; +import requireAuth from 'ember-osf-web/decorators/require-auth'; +// eslint-disable-next-line ember/no-mixins +import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; +import { action, computed } from '@ember/object'; +import PreprintSubmit from 'ember-osf-web/preprints/submit/controller'; +import Intl from 'ember-intl/services/intl'; +import Transition from '@ember/routing/-private/transition'; + +@requireAuth() +export default class PreprintSubmitRoute extends Route.extend(ConfirmationMixin, {}) { + @service store!: Store; + @service intl!: Intl; + @service theme!: Theme; + @service router!: RouterService; + @service metaTags!: MetaTags; + headTags?: HeadTagDef[]; + + // This does NOT work on chrome and I'm going to leave it just in case + confirmationMessage = this.intl.t('preprints.submit.action-flow.save-before-exit'); + + 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; + return { + provider, + brand: provider.brand.content, + displayDialog: this.displayDialog, + }; + } catch (e) { + + this.router.transitionTo('not-found', `preprints/${args.provider_id}/submit`); + return null; + } + } + + afterModel(model: PreprintProviderModel) { + if (model && model.assets && model.assets.favicon) { + const headTags = [{ + type: 'link', + attrs: { + rel: 'icon', + href: model.assets.favicon, + }, + }]; + this.set('headTags', headTags); + } + } + + // This tells ember-onbeforeunload's ConfirmationMixin whether or not to stop transitions + // This is for when the user leaves the site or does a full app reload + @computed('controller.isPageDirty') + get isPageDirty() { + const controller = this.controller as PreprintSubmit; + return () => controller.isPageDirty; + } + + // This is for when the user leaves the page via the router + @action + willTransition(transition: Transition) { + const controller = this.controller as PreprintSubmit; + if (controller.isPageDirty) { + if (!window.confirm(this.intl.t('preprints.submit.action-flow.save-before-exit'))) { + transition.abort(); + } + } + } +} diff --git a/app/preprints/submit/template.hbs b/app/preprints/submit/template.hbs new file mode 100644 index 00000000000..5e535341e87 --- /dev/null +++ b/app/preprints/submit/template.hbs @@ -0,0 +1,11 @@ +{{#if this.model.provider}} + +{{else}} + +{{/if}} \ No newline at end of file diff --git a/app/router.ts b/app/router.ts index c3c6e20d238..21592fb73e1 100644 --- a/app/router.ts +++ b/app/router.ts @@ -31,6 +31,10 @@ Router.map(function() { this.route('index', { path: '/' }); this.route('discover', { path: '/:provider_id/discover' }); this.route('detail', { path: '/:provider_id/:guid' }); + this.route('submit', { path: '/:provider_id/submit' }); + this.route('edit', { path: '/:provider_id/edit/:guid' }); + this.route('select'); + this.route('my-preprints'); }); diff --git a/app/search/controller.ts b/app/search/controller.ts index 24625559b86..fbfa7ec1f15 100644 --- a/app/search/controller.ts +++ b/app/search/controller.ts @@ -6,7 +6,7 @@ import { } from 'osf-components/components/search-page/component'; export default class SearchController extends Controller { - @tracked cardSearchText?: string = ''; + @tracked q?: string = ''; @tracked sort?: string = '-relevance'; @tracked resourceType?: ResourceTypeFilterValue | null = null; @tracked activeFilters?: Filter[] = []; diff --git a/app/serializers/contributor.ts b/app/serializers/contributor.ts index bedeeb1301f..6f8bea9c988 100644 --- a/app/serializers/contributor.ts +++ b/app/serializers/contributor.ts @@ -6,6 +6,7 @@ export default class ContributorSerializer extends OsfSerializer { const serialized = super.serialize(snapshot, options); delete serialized!.data!.relationships!.node; delete serialized!.data!.relationships!.draft_registration; + delete serialized!.data!.relationships!.preprint; return serialized; } diff --git a/app/services/theme.ts b/app/services/theme.ts index 679c52503aa..1df05939904 100644 --- a/app/services/theme.ts +++ b/app/services/theme.ts @@ -86,16 +86,12 @@ export default class Theme extends Service { return this.isProvider && !this.isDomain; } - @computed('id', 'isDomain', 'isProvider', 'settings.routePath') + @computed('id', 'isDomain', 'settings.routePath') get pathPrefix(): string { let pathPrefix = '/'; if (!this.isDomain) { - pathPrefix += `${this.settings.routePath}/`; - - if (this.isProvider) { - pathPrefix += `${this.id}/`; - } + pathPrefix += `${this.settings.routePath}/${this.id}/`; } return pathPrefix; diff --git a/app/styles/_accessibility.scss b/app/styles/_accessibility.scss index a3236b239c3..c905a0d2075 100644 --- a/app/styles/_accessibility.scss +++ b/app/styles/_accessibility.scss @@ -1,4 +1,95 @@ // stylelint-disable selector-class-pattern + +.btn { + display: inline-block; + margin-bottom: 0; + text-align: center; + white-space: nowrap; + vertical-align: middle; + touch-action: manipulation; + cursor: pointer; + border: 1px solid transparent; + font-size: 14px; + line-height: $tall-line-height; + border-radius: 2px; + user-select: none; + color: $color-text-white; + + &:active { + // TODO: color variable + box-shadow: inset 0 1px 2px rgba(38, 57, 71, 0.2); + } + + &:disabled { + cursor: not-allowed; + filter: alpha(opacity=65); + opacity: 0.65; + } +} + +.btn-small { + padding: 2px 10px; +} + +.btn-medium { + padding: 6px 12px; +} + +.btn-large { + padding: 15px 20px; +} + +.btn-primary { + background-color: var(--primary-color); + color: $color-text-white; + font-weight: bold; + + &:hover:not([disabled]) { + background-color: var(--secondary-color); + } +} + +// This should only be used in preprint branding as we move away from using custom CSS +// Please don't rely on this class for new brands +.DarkText { + color: $color-text-black; +} + +.btn-secondary { + background-color: $color-bg-white; + border: 1px solid $color-border-gray-light; + color: $color-text-black; + + &:hover:not([disabled]) { + border: 1px solid $color-bg-gray-blue; + background-color: $color-bg-gray-blue-light; + } +} + +.btn-create { + background-color: darken($brand-success, 10%); + color: $color-text-white; + + &:hover:not([disabled]) { + background-color: darken($brand-success, 25%); + } +} + +.btn-default { + color: $color-text-black; +} + +.btn-destroy { + background-color: $color-text-white; + color: $brand-danger; + border: 1px solid $color-border-gray-darker; + + &:hover:not([disabled]) { + background-color: $brand-danger; + color: $color-text-white; + } +} + .btn-success { color: $btn-success-high-contrast-color; background-color: $brand-success; @@ -9,6 +100,7 @@ border-color: darken($brand-success, 15%); } + &[disabled], &[disabled]:hover, :global(&.disabled), diff --git a/app/styles/_variables.scss b/app/styles/_variables.scss index 83593eba86d..fa92eb56358 100644 --- a/app/styles/_variables.scss +++ b/app/styles/_variables.scss @@ -48,6 +48,7 @@ $color-border-gray-dark: #e5e5e5; $color-border-gray: #ddd; $color-border-gray-light: #d9d9d9; $color-border-gray-cool: #d6dbdc; +$color-border-blue-dark: #263947; $color-gradient-primary: #eee; $color-gradient-secondary: #ccc; @@ -84,6 +85,7 @@ $color-bg-blue-dark: #337ab7; $color-bg-blue-darker: #214661; $color-bg-blue-light: #def; $color-bg-blue-lighter: rgba($color-bg-blue-dark, 0.2); +$color-bg-blue-highlight: #15a5eb; $color-bg-red: #a00; $color-bg-white-transparent: rgba(255, 255, 255, 0.49); $color-bg-success: #dff0d8; diff --git a/app/validators/http-url.ts b/app/validators/http-url.ts deleted file mode 100644 index e9f292f7a44..00000000000 --- a/app/validators/http-url.ts +++ /dev/null @@ -1,31 +0,0 @@ -import BaseValidator from 'ember-cp-validations/validators/base'; -import { regularExpressions } from 'ember-validators/format'; - -export interface HttpUrlValidatorOptions { - requireHttps: boolean; -} - -const httpRegExes = { - // http OR https - http: /^https?:\/\/[a-zA-Z0-9]/, - // https only - https: /^https:\/\/[a-zA-Z0-9]/, - // accept either for localhost or 127.0.0.1 - localhost: /^https?:\/\/(?:localhost|127\.0\.0\.1)(?:[/:?]|$)/, -}; - -export default class HttpUrlValidator extends BaseValidator { - validate(value: string, options: HttpUrlValidatorOptions = { requireHttps: false }) { - const httpRegEx = options.requireHttps ? httpRegExes.https : httpRegExes.http; - return ( - ( - (httpRegEx.test(value) && regularExpressions.url.test(value)) - || httpRegExes.localhost.test(value) - ) - || this.createErrorMessage( - options.requireHttps ? 'https_url' : 'url', - value, - ) - ); - } -} diff --git a/app/validators/url-with-protocol.ts b/app/validators/url-with-protocol.ts new file mode 100644 index 00000000000..8a2a738c8df --- /dev/null +++ b/app/validators/url-with-protocol.ts @@ -0,0 +1,49 @@ +import buildMessage from 'ember-changeset-validations/utils/validation-errors'; +import { validateFormat } from 'ember-changeset-validations/validators'; + +const Protocols = { + http: 'http:', + https: 'https:', +}; + +interface Options { + acceptedProtocols?: string[]; + translationArgs?: { + description: string, + }; +} + +export function validateUrlWithProtocols(options?: Options) { + return (key: string, newValue: string, _: any, __: any, ___: any) => { + const acceptedProtocols = options?.acceptedProtocols || [Protocols.http, Protocols.https]; + const translationArgs = options?.translationArgs || { description: '' }; + + let url; + try { + url = new URL(newValue); + } catch (e) { + return buildMessage(key, { + type: 'url', + context: { + type: 'url', + translationArgs, + }, + }); + } + + if (acceptedProtocols.indexOf(url.protocol) === -1) { + return buildMessage(key, { + type: 'url', + context: { + type: 'url', + translationArgs, + }, + }); + } + return validateFormat({ + allowBlank: false, + type: 'url', + translationArgs, + })(key, newValue, _, __, ___); + }; +} diff --git a/lib/app-components/addon/components/branded-navbar/component.ts b/lib/app-components/addon/components/branded-navbar/component.ts index ce62cde5e4d..02c916d1e96 100644 --- a/lib/app-components/addon/components/branded-navbar/component.ts +++ b/lib/app-components/addon/components/branded-navbar/component.ts @@ -49,10 +49,6 @@ export default class BrandedNavbar extends Component { return `${osfURL}reviews`; } - get submitPreprintUrl() { - return this.theme.isProvider ? `${osfURL}preprints/${this.theme.id}/submit/` : `${osfURL}preprints/submit/`; - } - @alias('theme.provider') provider!: ProviderModel; @alias('theme.provider.id') providerId!: string; @alias('theme.provider.brand.primaryColor') brandPrimaryColor!: BrandModel; diff --git a/lib/app-components/addon/components/branded-navbar/template.hbs b/lib/app-components/addon/components/branded-navbar/template.hbs index f754bd92761..ce625d54020 100644 --- a/lib/app-components/addon/components/branded-navbar/template.hbs +++ b/lib/app-components/addon/components/branded-navbar/template.hbs @@ -9,7 +9,7 @@ >
\ No newline at end of file diff --git a/lib/osf-components/addon/components/contributors/user-search/list/template.hbs b/lib/osf-components/addon/components/contributors/user-search/list/template.hbs index d372c266153..17691ef4c48 100644 --- a/lib/osf-components/addon/components/contributors/user-search/list/template.hbs +++ b/lib/osf-components/addon/components/contributors/user-search/list/template.hbs @@ -1,15 +1,4 @@ {{#if @results}} -
- - {{t 'osf-components.contributors.headings.name'}} - - - {{t 'osf-components.contributors.headings.permission'}} - - - {{t 'osf-components.contributors.headings.citation'}} - -
{{#each @results as |result|}} {{else if @fetchUsers.last.isSuccessful}}

- {{t 'registries.registration_metadata.add_contributors.no_results'}} + {{t 'registries.registration_metadata.add_contributors.no-results'}}

-{{else}} - {{t 'registries.registration_metadata.add_contributors.help_text' htmlSafe=true}} {{/if}} diff --git a/lib/osf-components/addon/components/contributors/user-search/widget/component.ts b/lib/osf-components/addon/components/contributors/user-search/widget/component.ts index 3d6e1ee58b8..0e0a609c10f 100644 --- a/lib/osf-components/addon/components/contributors/user-search/widget/component.ts +++ b/lib/osf-components/addon/components/contributors/user-search/widget/component.ts @@ -1,5 +1,5 @@ import Store from '@ember-data/store'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; import Component from '@glimmer/component'; @@ -27,6 +27,7 @@ export default class UserSearchComponent extends Component -
- - -
- -
-
- - {{t 'registries.registration_metadata.add_contributors.results_heading'}} - +
+
+
- +
+ +
+ {{#if this.displayResults}} +
+ +
+
+ +
+ {{/if}}
diff --git a/lib/osf-components/addon/components/contributors/widget/styles.scss b/lib/osf-components/addon/components/contributors/widget/styles.scss index 21128c9ca58..bcd2c34f678 100644 --- a/lib/osf-components/addon/components/contributors/widget/styles.scss +++ b/lib/osf-components/addon/components/contributors/widget/styles.scss @@ -1,31 +1,68 @@ -.Container { +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.warning-container { + margin-top: 10px; + color: $brand-danger; +} + +.display-container { border: 1px solid $color-border-gray; margin-top: 10px; overflow-y: scroll; + overflow-x: hidden; max-height: 500px; -} -.Heading { - padding: 11px 20px; - height: 40px; - font-weight: bold; - display: flex; - border-bottom: 1px solid $color-border-gray; - background-color: #fff; - position: sticky; - top: 0; - z-index: 1; + .heading-container { + width: 100%; + height: 40px; + font-weight: bold; + border-bottom: 1px solid $color-border-gray; + background-color: #fff; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + .name-title, + .permission-title, + .citation-title { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .name-title { + padding-left: 40px; + width: 50%; + } + + .permission-title { + width: 30%; + } + + .citation-title { + width: 20%; + } + + &.mobile { + .name-title { + padding-left: 5px; + width: 100%; + } + } + } } -.HeadingTitle { - flex: 1 0 auto; - max-width: 30%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +.add-user-container { + margin-top: 10px; + + .user-search-container { + margin-top: 10px; + max-height: 425px; + overflow: hidden; + } - &:first-of-type { - flex: 2 0 auto; - max-width: 41%; + &.mobile { + max-height: 450px; } } diff --git a/lib/osf-components/addon/components/contributors/widget/template.hbs b/lib/osf-components/addon/components/contributors/widget/template.hbs index de9972815bb..16b79d578de 100644 --- a/lib/osf-components/addon/components/contributors/widget/template.hbs +++ b/lib/osf-components/addon/components/contributors/widget/template.hbs @@ -1,34 +1,44 @@ - {{#let (unique-id 'current-contributors') as |currentContributorFieldId|}} - {{#if @shouldShowAdd}} - - - {{/if}} -
-
- - {{t 'osf-components.contributors.headings.name'}} - - + {{#if @displayPermissionWarning}} +
+ {{t 'osf-components.contributors.permission-warning'}} +
+ {{/if}} +
+
+
+ {{t 'osf-components.contributors.headings.name'}} +
+ {{#if (not (is-mobile))}} +
{{t 'osf-components.contributors.headings.permission'}} - - +
+
{{t 'osf-components.contributors.headings.citation'}} - +
+ {{/if}} +
+ +
+ {{#if @shouldShowAdd}} +
+ +
+
-
- {{/let}} + {{/if}} diff --git a/lib/osf-components/addon/components/delete-button/component.ts b/lib/osf-components/addon/components/delete-button/component.ts index d903ce1eaee..8ff43f72fc0 100644 --- a/lib/osf-components/addon/components/delete-button/component.ts +++ b/lib/osf-components/addon/components/delete-button/component.ts @@ -30,6 +30,7 @@ export default class DeleteButton extends Component { small = false; secondary = false; smallSecondary = false; + buttonLayout = 'medium'; noBackground = false; hardConfirm = false; disabled = false; diff --git a/lib/osf-components/addon/components/delete-button/template.hbs b/lib/osf-components/addon/components/delete-button/template.hbs index 15f43835e8b..1bd12fce8b4 100644 --- a/lib/osf-components/addon/components/delete-button/template.hbs +++ b/lib/osf-components/addon/components/delete-button/template.hbs @@ -7,6 +7,7 @@ @type='destroy' {{on 'click' this._show}} @disabled={{this.disabled}} + ...attributes > @@ -19,6 +20,7 @@ @type='secondary' @layout={{if this.smallSecondary 'small' 'medium'}} {{on 'click' this._show}} + ...attributes > {{#if @icon}} @@ -29,9 +31,11 @@ diff --git a/lib/osf-components/addon/components/dropzone-widget/component.ts b/lib/osf-components/addon/components/dropzone-widget/component.ts index 3848d9db7d9..5eb14876a99 100644 --- a/lib/osf-components/addon/components/dropzone-widget/component.ts +++ b/lib/osf-components/addon/components/dropzone-widget/component.ts @@ -79,6 +79,7 @@ export default class DropzoneWidget extends Component.extend({ defaultMessage = this.intl.t('dropzone_widget.drop_files'); @requiredAction buildUrl!: (files: File[]) => void; + success?: (context: any, drop: any, file: any) => Promise; preUpload?: (context: any, drop: any, file: any) => Promise; didInsertElement() { diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts b/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts index aa5f8ba093f..819e68903fe 100644 --- a/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts @@ -1,38 +1,36 @@ import { tagName } from '@ember-decorators/component'; import Component from '@ember/component'; import { assert } from '@ember/debug'; -import { action } from '@ember/object'; import { BufferedChangeset } from 'ember-changeset/types'; import { layout } from 'ember-osf-web/decorators/component'; +import { SchemaBlock } from 'ember-osf-web/packages/registration-schema'; import styles from './styles'; import template from './template'; +export interface RadioButtonOption { + displayText: string; + inputValue: string | boolean | number; +} + @tagName('') @layout(template, styles) -export default class FormControlRadioButton extends Component { +export default class FormControlRadioButtonGroup extends Component { // Required params - options!: string[]; + options!: string[] | SchemaBlock[]; valuePath!: string; changeset!: BufferedChangeset; // Optional params + helpTextMapping?: any; shouldShowMessages?: boolean; disabled = false; - onchange?: (option: string) => void; + onchange?: (option: string | number | boolean) => void; didReceiveAttrs() { assert('FormControls::RadioButton - @options are required', Boolean(this.options)); assert('FormControls::RadioButton - @valuePath is required', Boolean(this.valuePath)); assert('FormControls::RadioButton - @changeset is required', Boolean(this.changeset)); } - - @action - updateChangeset(option: string) { - this.changeset.set(this.valuePath, option); - if (this.onchange) { - this.onchange(option); - } - } } diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/component.ts b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/component.ts new file mode 100644 index 00000000000..2e9c939a5ae --- /dev/null +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/component.ts @@ -0,0 +1,47 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { BufferedChangeset } from 'ember-changeset/types'; + +import { RadioButtonOption } from 'osf-components/components/form-controls/radio-button-group/component'; + +/** + * The Radio Button Args + */ +interface RadioButtonArgs{ + option: string | RadioButtonOption; + valuePath: string; + changeset: BufferedChangeset; + disabled: boolean; + helpTextMapping?: any; + onchange?: (_: string | number | boolean) => void; +} + +export default class FormControlRadioButton extends Component { + public get displayText(): string | number | boolean { + if (typeof this.args.option === 'string') { + return this.args.option; + } else { + return this.args.option.displayText !== undefined ? this.args.option.displayText : ''; + } + } + + public get isValueChecked(): boolean { + return this.args.changeset.get(this.args.valuePath) === this.getValue; + } + + public get getValue(): string | number | boolean { + if (typeof this.args.option === 'string') { + return this.args.option; + } else { + return this.args.option.inputValue !== undefined ? this.args.option.inputValue : ''; + } + } + + @action + public updateChangeset(): void { + this.args.changeset.set(this.args.valuePath, this.getValue); + if (this.args.onchange) { + this.args.onchange(this.getValue); + } + } +} diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/styles.scss b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/styles.scss new file mode 100644 index 00000000000..73037f48038 --- /dev/null +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/styles.scss @@ -0,0 +1,13 @@ +.RadioButton { + display: flex; + + .input { + flex: 0 0 auto; + } + + .RadioLabel { + flex: 1 0 0; + margin-left: 10px; + font-weight: 500; + } +} diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/template.hbs b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/template.hbs new file mode 100644 index 00000000000..ded859ab573 --- /dev/null +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/template.hbs @@ -0,0 +1,22 @@ +
+ {{#let (unique-id 'radio' ) as |uniqueId|}} + + + {{/let}} +
\ No newline at end of file diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss b/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss index bb988db496b..87b17f9c150 100644 --- a/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss @@ -1,20 +1,12 @@ -.RadioButtonGroup { - padding-left: 10px; -} +// stylelint-disable max-nesting-depth, selector-max-compound-selectors -.RadioButton { +.RadioButtonContainer { display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; - input { - flex: 0 0 auto; - } - - label { - flex: 1 0 0; - margin-left: 10px; + .RadioButtonGroup { + padding-left: 10px; } } - -.RadioLabel { - font-weight: 500; -} diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs b/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs index e87153edd06..292781d887f 100644 --- a/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs @@ -1,33 +1,26 @@ -
- {{#each this.options as |option|}} - {{#let (unique-id 'radio' option) as |uniqueId|}} +
+
+ {{#each this.options as |option|}}
- - +
- {{/let}} - {{/each}} + {{/each}} +
{{#if @shouldShowMessages}} {{/if}} - {{#if @manager.node.isRegistration}} -
-
-
{{t 'osf-components.node-metadata-form.subjects'}}
-
- +
+
+
{{t 'osf-components.node-metadata-form.subjects'}}
+
+ + {{#if subjectsManager.loadingNodeSubjects}} + + {{else}} - -
-
-
- {{/if}} + {{/if}} +
+
+
+
{{t 'osf-components.node-metadata-form.tags'}}
diff --git a/lib/osf-components/addon/components/node-picker/component.ts b/lib/osf-components/addon/components/node-picker/component.ts new file mode 100644 index 00000000000..8f0c321aba1 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/component.ts @@ -0,0 +1,123 @@ +import Component from '@glimmer/component'; +import Store from '@ember-data/store'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import { restartableTask, timeout } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import { stripDiacritics } from 'ember-power-select/utils/group-utils'; +import Node from 'ember-osf-web/models/node'; +import CurrentUser from 'ember-osf-web/services/current-user'; +import { tracked } from '@glimmer/tracking'; + +function stripAndLower(text: string): string { + return stripDiacritics(text).toLowerCase(); +} + +/** + * The node picker args + */ +interface NodePickerArgs { + projectSelected: (value: Node) => void; +} + +export default class NodePicker extends Component { + @service currentUser!: CurrentUser; + @service store!: Store; + + @tracked selected: Node | null = null; + filter = ''; + page = 1; + @tracked hasMore = false; + @tracked loadingMore = false; + @tracked items: Node[] = []; + + constructor(owner: unknown, args: NodePickerArgs) { + super(owner, args); + + this.selected = null; + this.filter = ''; + this.page = 1; + + taskFor(this.findNodes).perform(); + } + + @restartableTask + @waitFor + async findNodes(filter = '') { + if (filter) { + await timeout(250); + } + + const { user } = this.currentUser; + + if (!user) { + return []; + } + + // If the filter changed, reset the page number + if (filter !== this.filter) { + this.filter = filter; + this.page = 1; + } + + const more = this.page > 1; + + if (more) { + this.loadingMore = true; + } + + const nodes = await user.queryHasMany('nodes', { + filter: { + title: this.filter ? this.filter : undefined, + }, + page: this.page, + }); + + const { meta } = nodes; + this.hasMore = meta.total > meta.per_page * this.page; + const items = more ? this.items.concat(nodes) : nodes; + + this.items = items; + this.loadingMore = false; + + return items; + } + + /** + * Passed into power-select component for customized searching. + * + * @returns results if match in node, root, or parent title + */ + matcher(option: Node, searchTerm: string): -1 | 1 { + const sanitizedTerm = stripAndLower(searchTerm); + + const hasTerm = [ + option.title, + option.root && option.root.title, + option.parent && option.parent.title, + ].some(field => !!field && stripAndLower(field).includes(sanitizedTerm)); + + return hasTerm ? 1 : -1; + } + + @action + valueChanged(value?: Node): void { + if (value) { + this.selected = value; + this.args.projectSelected(value); + } + } + + @action + loadMore(this: NodePicker): Promise { + this.page += 1; + + return taskFor(this.findNodes).perform(); + } + + @action + oninput(this: NodePicker, term: string): true | Promise { + return !!term || taskFor(this.findNodes).perform(); + } +} diff --git a/lib/osf-components/addon/components/node-picker/load-more-component/component.ts b/lib/osf-components/addon/components/node-picker/load-more-component/component.ts new file mode 100644 index 00000000000..f88fbe98da5 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/load-more-component/component.ts @@ -0,0 +1,13 @@ +import Component from '@glimmer/component'; + +/** + * The Load More Node Args + */ +interface LoadMoreNodeArgs { + isLoading: boolean; + hasMore: boolean; + loadMore: () => void; +} + +// eslint-disable-next-line ember/no-empty-glimmer-component-classes +export default class NodePickerLoadMoreComponent extends Component { } diff --git a/lib/osf-components/addon/components/node-picker/load-more-component/styles.scss b/lib/osf-components/addon/components/node-picker/load-more-component/styles.scss new file mode 100644 index 00000000000..af53d040f72 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/load-more-component/styles.scss @@ -0,0 +1,12 @@ +.nobullet { + list-style: none; +} + +.text-center { + text-align: center; +} + +.ember-power-select-option { + cursor: pointer; + padding: 0 8px; +} diff --git a/lib/osf-components/addon/components/node-picker/load-more-component/template.hbs b/lib/osf-components/addon/components/node-picker/load-more-component/template.hbs new file mode 100644 index 00000000000..c1c0b90b004 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/load-more-component/template.hbs @@ -0,0 +1,12 @@ +{{#if @isLoading}} + {{t 'node.projects.load-more.loading'}} +{{else if @hasMore}} +
  • + {{t 'node.projects.load-more.load-more'}} +
  • +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/node-picker/styles.scss b/lib/osf-components/addon/components/node-picker/styles.scss new file mode 100644 index 00000000000..e4df17123b6 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/styles.scss @@ -0,0 +1,4 @@ +.form-group { + margin-bottom: 15px; + margin-top: 10px; +} diff --git a/lib/osf-components/addon/components/node-picker/template.hbs b/lib/osf-components/addon/components/node-picker/template.hbs new file mode 100644 index 00000000000..457c5cc8c66 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/template.hbs @@ -0,0 +1,28 @@ +{{#if this.findNodes.last}} +
    + + {{get-ancestor-descriptor item}} {{item.title}} + +
    +{{else}} + +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs b/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs index 4ac7dc2d870..678af063fb3 100644 --- a/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs +++ b/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs @@ -1,6 +1,6 @@
  • @@ -20,8 +20,9 @@ {{/if}}
  • {{t 'navbar.add_a_preprint' preprintWord=(t 'documentType.preprint.singularCapitalized')}} diff --git a/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts b/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts index c3eaa7891c1..e5c744a03f7 100644 --- a/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts +++ b/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts @@ -42,18 +42,33 @@ export default class SubjectBrowserManagerComponent extends Component { @waitFor async loadRootSubjects() { try { - const provider = await this.subjectsManager.provider; - const rootSubjects = await provider.queryHasMany('subjects', { - filter: { - parent: 'null', - }, - page: { - size: subjectPageSize, - }, - sort: 'text', - related_counts: 'children', - }); - this.setProperties({ rootSubjects }); + if (this.subjectsManager.provider) { + const provider = await this.subjectsManager.provider; + const rootSubjects = await provider.queryHasMany('subjects', { + filter: { + parent: 'null', + }, + page: { + size: subjectPageSize, + }, + sort: 'text', + related_counts: 'children', + }); + this.setProperties({ rootSubjects }); + } else { + const model = this.subjectsManager.model; + const rootSubjects = await model.queryHasMany('subjectsAcceptable', { + filter: { + parent: 'null', + }, + page: { + size: subjectPageSize, + }, + sort: 'text', + related_counts: 'children', + }); + this.setProperties({ rootSubjects }); + } } catch (e) { const errorMessage = this.intl.t('registries.registration_metadata.load_subjects_error'); captureException(e, { errorMessage }); diff --git a/lib/osf-components/addon/components/subjects/manager/component.ts b/lib/osf-components/addon/components/subjects/manager/component.ts index 7b74acd5a7b..7f1ec9e08ec 100644 --- a/lib/osf-components/addon/components/subjects/manager/component.ts +++ b/lib/osf-components/addon/components/subjects/manager/component.ts @@ -23,6 +23,8 @@ import template from './template'; interface ModelWithSubjects extends OsfModel { subjects: SubjectModel[]; + subjectsAcceptable?: SubjectModel[]; + isProject: boolean; } // SubjectManager is responsible for: @@ -34,7 +36,8 @@ export interface SubjectManager { savedSubjects: SubjectModel[]; isSaving: boolean; hasChanged: boolean; - provider: ProviderModel; + provider?: ProviderModel; + model: ModelWithSubjects; selectSubject(subject: SubjectModel): void; unselectSubject(subject: SubjectModel): void; @@ -51,17 +54,20 @@ export interface SubjectManager { export default class SubjectManagerComponent extends Component { // required model!: ModelWithSubjects; - provider!: ProviderModel; + provider?: ProviderModel; doesAutosave!: boolean; // optional metadataChangeset?: BufferedChangeset; + onchange?: () => void; + hasSubjects?: (_: boolean) => void; // private @service intl!: Intl; @service toast!: Toast; @service store!: Store; + savedSubjectIds = new Set(); selectedSubjectIds = new Set(); @@ -115,6 +121,11 @@ export default class SubjectManagerComponent extends Component { }); this.incrementProperty('selectedSubjectsChanges'); this.incrementProperty('savedSubjectsChanges'); + this.model.set('subjects', savedSubjects); + if (this.hasSubjects) { + this.metadataChangeset?.validate('subjects'); + this.hasSubjects(savedSubjectIds.size > 0); + } } @restartableTask @@ -136,6 +147,11 @@ export default class SubjectManagerComponent extends Component { if (this.metadataChangeset) { this.metadataChangeset.validate('subjects'); } + + if (this.onchange) { + this.onchange(); + } + } catch (e) { const errorMessage = this.intl.t('registries.registration_metadata.save_subjects_error'); captureException(e, { errorMessage }); @@ -156,8 +172,15 @@ export default class SubjectManagerComponent extends Component { super.init(); assert('@model is required', Boolean(this.model)); - assert('@provider is required', Boolean(this.provider)); assert('@doesAutosave is required', this.doesAutosave !== null && this.doesAutosave !== undefined); + const isProject = this.model.get('isProject'); + if (!isProject) { + assert('@provider is required', Boolean(this.provider)); + } + + if (isProject) { + assert('@subjectsAcceptable is required', this.model.get('subjectsAcceptable') !== undefined); + } } @action @@ -187,7 +210,7 @@ export default class SubjectManagerComponent extends Component { this.incrementProperty('selectedSubjectsChanges'); // assumes the parent is already loaded in the store, which at the moment is true - if (subject.parent) { + if (subject.parent ) { this.selectSubject(subject.parent); } } diff --git a/lib/osf-components/addon/components/subjects/manager/template.hbs b/lib/osf-components/addon/components/subjects/manager/template.hbs index b844adfde5c..58b78939ea8 100644 --- a/lib/osf-components/addon/components/subjects/manager/template.hbs +++ b/lib/osf-components/addon/components/subjects/manager/template.hbs @@ -4,6 +4,7 @@ isSaving=this.saveChanges.isRunning hasChanged=this.hasChanged provider=this.provider + model=this.model selectSubject=(action this.selectSubject) unselectSubject=(action this.unselectSubject) diff --git a/lib/osf-components/addon/components/subjects/search/component.ts b/lib/osf-components/addon/components/subjects/search/component.ts index 3b78f7a9ee0..9478c0a51de 100644 --- a/lib/osf-components/addon/components/subjects/search/component.ts +++ b/lib/osf-components/addon/components/subjects/search/component.ts @@ -22,7 +22,6 @@ export default class SearchSubjects extends Component { @alias('doSearch.isRunning') isLoading!: boolean; - @alias('doSearch.lastSuccessful.value') searchResults?: SubjectModel[]; @computed('searchResults.[]') @@ -36,24 +35,42 @@ export default class SearchSubjects extends Component { async doSearch() { await timeout(500); // debounce - const provider = await this.subjectsManager.provider; - const { userQuery } = this; if (!userQuery) { return undefined; } - const filterResults = await provider.queryHasMany('subjects', { - filter: { - text: userQuery, - }, - page: { - size: 150, - }, - sort: 'text', - related_counts: 'children', - embed: 'parent', - }); - - return filterResults; + + if (this.subjectsManager.model.get('isProject')) { + const model = this.subjectsManager.model; + const filterResults = await model.queryHasMany('subjectsAcceptable', { + filter: { + text: userQuery, + }, + page: { + size: 150, + }, + sort: 'text', + related_counts: 'children', + }); + + this.set('searchResults', filterResults); + return filterResults; + } else { + const provider = await this.subjectsManager.provider; + const filterResults = await provider.queryHasMany('subjects', { + filter: { + text: userQuery, + }, + page: { + size: 150, + }, + sort: 'text', + related_counts: 'children', + embed: 'parent', + }); + + this.set('searchResults', filterResults); + return filterResults; + } } } diff --git a/lib/osf-components/addon/components/tags-widget/component.ts b/lib/osf-components/addon/components/tags-widget/component.ts index 6679bb35326..2210d3e11db 100644 --- a/lib/osf-components/addon/components/tags-widget/component.ts +++ b/lib/osf-components/addon/components/tags-widget/component.ts @@ -14,6 +14,7 @@ import template from './template'; interface Taggable extends OsfModel { tags: string[]; + isTagClickable: boolean; } @layout(template, styles) @@ -26,6 +27,7 @@ export default class TagsWidget extends Component.extend({ styles }) { // optional arguments readOnly = true; autoSave = true; + isTagClickable = true; onChange?: (taggable: Taggable) => void; @attribute('data-analytics-scope') @@ -39,6 +41,7 @@ export default class TagsWidget extends Component.extend({ styles }) { assert('tags-widget: You must pass in a taggable model', Boolean(this.taggable && 'tags' in this.taggable)); } + @action _addTag(tag: string) { this.analytics.trackFromElement(this.element, { @@ -63,7 +66,9 @@ export default class TagsWidget extends Component.extend({ styles }) { @action _clickTag(tag: string): void { - this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); + if (this.isTagClickable) { + this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); + } } _onChange() { diff --git a/lib/osf-components/addon/components/tags-widget/styles.scss b/lib/osf-components/addon/components/tags-widget/styles.scss index 085a150bfca..d76dcf10361 100644 --- a/lib/osf-components/addon/components/tags-widget/styles.scss +++ b/lib/osf-components/addon/components/tags-widget/styles.scss @@ -1,14 +1,15 @@ .TagsWidget.TagsWidget { - border: 0; + border: 0 !important; padding: 0; margin: 0; + margin-top: 10px; } .TagsWidget :global(.emberTagInput-tag) { background: $color-bg-blue-light; border-radius: 0; color: $color-text-black; - cursor: pointer; + cursor: default; font-size: 13px; max-width: 100%; overflow-wrap: break-word; @@ -27,6 +28,10 @@ } } +.TagsWidget :global(.emberTagInput-tag .cursor) { + cursor: pointer; +} + .TagsWidget :global(.emberTagInput-new) { width: 100%; diff --git a/lib/osf-components/addon/components/tags-widget/template.hbs b/lib/osf-components/addon/components/tags-widget/template.hbs index 54b9e60ae9f..63475ed2e2c 100644 --- a/lib/osf-components/addon/components/tags-widget/template.hbs +++ b/lib/osf-components/addon/components/tags-widget/template.hbs @@ -1,7 +1,7 @@ {{#unless @taggable.tags.length}} - +
    {{t 'osf-components.tags-widget.no_tags'}} - +
    {{/unless}} + {{!-- template-lint-disable no-invalid-interactive --}} {{tag}} diff --git a/lib/osf-components/addon/components/validated-input/base-component.ts b/lib/osf-components/addon/components/validated-input/base-component.ts index 5a2eaa7d5d6..e6362bc99d8 100644 --- a/lib/osf-components/addon/components/validated-input/base-component.ts +++ b/lib/osf-components/addon/components/validated-input/base-component.ts @@ -45,15 +45,15 @@ export default abstract class BaseValidatedInput extends Compon @computed('errors', 'validation.options', 'isRequired') get required(): boolean { - if (!this.validation) { - return false; - } if (this.isRequired === true) { return true; } if (this.isRequired === false) { return false; } + if (!this.validation) { + return false; + } const { options } = this.validation; if (!options) { return false; 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 5921966c4b9..28466252c8d 100644 --- a/lib/osf-components/addon/components/validated-input/text/template.hbs +++ b/lib/osf-components/addon/components/validated-input/text/template.hbs @@ -19,7 +19,9 @@ local-class='PrefixedInput' @type={{if this.password 'password' 'text'}} @value={{this.value}} + maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} + ...attributes />
  • {{else}} @@ -32,7 +34,9 @@ class='form-control' @type={{if this.password 'password' 'text'}} @value={{this.value}} + maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} + ...attributes /> {{/if}} {{/validated-input/x-input-wrapper}} diff --git a/lib/osf-components/addon/helpers/is-mobile.ts b/lib/osf-components/addon/helpers/is-mobile.ts new file mode 100644 index 00000000000..396836dba4b --- /dev/null +++ b/lib/osf-components/addon/helpers/is-mobile.ts @@ -0,0 +1,11 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; +import Media from 'ember-responsive'; + +export default class IsMobileHelper extends Helper { + @service media!: Media; + + compute(): boolean { + return this.media.isMobile; + } +} diff --git a/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/component.js b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/component.js new file mode 100644 index 00000000000..a1b4fb46d3a --- /dev/null +++ b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/form-controls/radio-button-group/radio-button/component'; diff --git a/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/template.js b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/template.js new file mode 100644 index 00000000000..b4b62ad8c67 --- /dev/null +++ b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/form-controls/radio-button-group/radio-button/template'; diff --git a/lib/osf-components/app/components/node-picker/component.js b/lib/osf-components/app/components/node-picker/component.js new file mode 100644 index 00000000000..a7714506134 --- /dev/null +++ b/lib/osf-components/app/components/node-picker/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/component'; diff --git a/lib/osf-components/app/components/node-picker/load-more-component/component.js b/lib/osf-components/app/components/node-picker/load-more-component/component.js new file mode 100644 index 00000000000..5dadf5b3576 --- /dev/null +++ b/lib/osf-components/app/components/node-picker/load-more-component/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/load-more-component/component'; diff --git a/lib/osf-components/app/components/node-picker/load-more-component/template.js b/lib/osf-components/app/components/node-picker/load-more-component/template.js new file mode 100644 index 00000000000..ee6eb61cd32 --- /dev/null +++ b/lib/osf-components/app/components/node-picker/load-more-component/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/load-more-component/template'; diff --git a/lib/osf-components/app/components/node-picker/template.js b/lib/osf-components/app/components/node-picker/template.js new file mode 100644 index 00000000000..9ae85c4532a --- /dev/null +++ b/lib/osf-components/app/components/node-picker/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/template'; diff --git a/lib/osf-components/app/helpers/is-mobile.js b/lib/osf-components/app/helpers/is-mobile.js new file mode 100644 index 00000000000..f7f42c39666 --- /dev/null +++ b/lib/osf-components/app/helpers/is-mobile.js @@ -0,0 +1 @@ +export { default } from 'osf-components/helpers/is-mobile'; diff --git a/lib/registries/addon/branded/discover/template.hbs b/lib/registries/addon/branded/discover/template.hbs index 6d6cd0793be..61cae81f6d8 100644 --- a/lib/registries/addon/branded/discover/template.hbs +++ b/lib/registries/addon/branded/discover/template.hbs @@ -9,7 +9,7 @@ @defaultQueryOptions={{this.defaultQueryOptions}} @resourceType={{'Registration,RegistrationComponent'}} @queryParams={{this.queryParams}} - @query={{this.q}} + @cardsearchText={{this.q}} @sort={{this.sort}} @onQueryParamChange={{action this.onQueryParamChange}} @showResourceTypeFilter={{false}} diff --git a/lib/registries/addon/branded/new/template.hbs b/lib/registries/addon/branded/new/template.hbs index f9f28e63489..3a7c3cb10bf 100644 --- a/lib/registries/addon/branded/new/template.hbs +++ b/lib/registries/addon/branded/new/template.hbs @@ -124,7 +124,7 @@ data-test-start-registration-button data-analytics-name={{if this.isBasedOnProject 'Create new draft registration' 'Create new no-project draft registration'}} local-class='createDraftButton' - disabled={{this.disableCreateDraft}} + disabled={{or this.disableCreateDraft this.createNewDraftRegistration.isRunning}} @type='primary' @layout='medium' {{on 'click' (perform this.createNewDraftRegistration)}} diff --git a/mirage/config.ts b/mirage/config.ts index e380d8f0fc1..444dfe5e2d1 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -10,7 +10,7 @@ import { createCollectionSubmission, getCollectionSubmissions } from './views/co import { createSubmissionAction } from './views/collection-submission-action'; import { searchCollections } from './views/collection-search'; import { reportDelete } from './views/comment'; -import { addContributor, createBibliographicContributor } from './views/contributor'; +import { addContributor, addPreprintContributor, createBibliographicContributor } from './views/contributor'; import { createDeveloperApp, updateDeveloperApp } from './views/developer-app'; import { createDraftRegistration } from './views/draft-registration'; import { @@ -28,7 +28,8 @@ import { postCountedUsage, getNodeAnalytics } from './views/metrics'; import { addCollectionModerator, addRegistrationModerator } from './views/moderator'; import { createNode, storageStatus } from './views/node'; import { osfNestedResource, osfResource, osfToManyRelationship } from './views/osf-resource'; -import { getProviderSubjects } from './views/provider-subjects'; +import { getPreprintProviderSubjects, getProviderSubjects } from './views/provider-subjects'; +import { getSubjectsAcceptable } from './views/subjects-acceptable'; import { createRegistration, forkRegistration, @@ -46,11 +47,13 @@ import { claimUnregisteredUser, userNodeList, userRegistrationList, + userPreprintList, } from './views/user'; import { updatePassword } from './views/user-password'; import * as userSettings from './views/user-setting'; import * as addons from './views/addons'; import * as wb from './views/wb'; +import { createPreprint } from './views/preprint'; const { OSF: { addonServiceUrl, apiUrl, shareBaseUrl, url: osfUrl } } = config; @@ -125,6 +128,7 @@ export default function(this: Server) { osfResource(this, 'subject', { only: ['show'] }); osfNestedResource(this, 'subject', 'children', { only: ['index'] }); osfNestedResource(this, 'node', 'children'); + this.get('/nodes/:parentID/subjectsAcceptable', getSubjectsAcceptable); osfNestedResource(this, 'node', 'contributors', { defaultSortKey: 'index', onCreate: createBibliographicContributor, @@ -315,6 +319,7 @@ export default function(this: Server) { this.get('/users/:id/nodes', userNodeList); this.get('/sparse/users/:id/nodes', userNodeList); this.get('/users/:id/registrations', userRegistrationList); + this.get('/users/:id/preprints', userPreprintList); osfNestedResource(this, 'user', 'draftRegistrations', { only: ['index'], }); @@ -328,11 +333,20 @@ export default function(this: Server) { path: '/providers/preprints/:parentID/subjects/highlighted/', relatedModelName: 'subject', }); + + osfNestedResource(this, 'preprint-provider', 'licensesAcceptable', { + only: ['index'], + path: '/providers/preprints/:parentID/licenses/', + relatedModelName: 'license', + }); + osfNestedResource(this, 'preprint-provider', 'preprints', { path: '/providers/preprints/:parentID/preprints/', relatedModelName: 'preprint', }); + this.get('/providers/preprints/:parentID/subjects/', getPreprintProviderSubjects); + osfNestedResource(this, 'preprint-provider', 'citationStyles', { only: ['index'], path: '/providers/preprints/:parentID/citation_styles/', @@ -344,11 +358,21 @@ export default function(this: Server) { */ osfResource(this, 'preprint'); + this.post('/preprints', createPreprint); + + this.get('/preprints/:id', (schema, request) => { + const id = request.params.id; + return schema.preprints.find(id); + }); + osfNestedResource(this, 'preprint', 'contributors', { path: '/preprints/:parentID/contributors/', defaultSortKey: 'index', - relatedModelName: 'contributor', + except: ['create'], }); + + this.post('/preprints/:preprintID/contributors/', addPreprintContributor); + osfNestedResource(this, 'preprint', 'bibliographicContributors', { path: '/preprints/:parentID/bibliographic_contributors/', defaultSortKey: 'index', @@ -359,16 +383,30 @@ export default function(this: Server) { defaultSortKey: 'index', 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', { path: '/wb/files/:fileID/', defaultSortKey: 'index', relatedModelName: 'file', }); - osfNestedResource(this, 'preprint', 'subjects', { - path: '/preprints/:parentID/subjects/', - defaultSortKey: 'index', - relatedModelName: 'subject', + + osfToManyRelationship(this, 'preprint', 'subjects', { + only: ['related', 'self', 'update'], }); + osfNestedResource(this, 'preprint', 'identifiers', { path: '/preprints/:parentID/identifiers/', defaultSortKey: 'index', diff --git a/mirage/factories/file.ts b/mirage/factories/file.ts index 05938d733d0..f84db04f1d2 100644 --- a/mirage/factories/file.ts +++ b/mirage/factories/file.ts @@ -12,7 +12,7 @@ export interface FileTraits { export interface PolymorphicTargetRelationship { id: ID; - type: 'draft-nodes' | 'nodes'; + type: 'draft-nodes' | 'nodes' | 'preprints'; } export interface MirageFile extends File { diff --git a/mirage/factories/preprint.ts b/mirage/factories/preprint.ts index 7225ad6850c..858e51b5a59 100644 --- a/mirage/factories/preprint.ts +++ b/mirage/factories/preprint.ts @@ -1,11 +1,11 @@ -import { Factory, Trait, trait } from 'ember-cli-mirage'; +import { Factory, ModelInstance, Trait, trait } from 'ember-cli-mirage'; import faker from 'faker'; import { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; import PreprintModel from 'ember-osf-web/models/preprint'; import { Permission } from 'ember-osf-web/models/osf-model'; import { ReviewsState } from 'ember-osf-web/models/provider'; - +import UserModel from 'ember-osf-web/models/user'; import { guid, guidAfterCreate} from './utils'; function buildLicenseText(): string { @@ -19,6 +19,8 @@ function buildLicenseText(): string { export interface PreprintMirageModel extends PreprintModel { isPreprintDoi: boolean; addLicenseName: boolean; + nodeId: number; + licenseId: number; } export interface PreprintTraits { @@ -29,6 +31,7 @@ export interface PreprintTraits { acceptedWithdrawalComment: Trait; rejectedWithdrawalNoComment: Trait; reviewAction: Trait; + withAffiliatedInstitutions: Trait; } export default Factory.extend({ @@ -39,7 +42,7 @@ export default Factory.extend({ addLicenseName: true, - currentUserPermissions: [Permission.Admin], + currentUserPermissions: [Permission.Admin, Permission.Write, Permission.Read], reviewsState: ReviewsState.REJECTED, @@ -61,6 +64,16 @@ export default Factory.extend({ year: '2023', }, + dateLastTransitioned: null, + hasCoi: null, + conflictOfInterestStatement: null, + hasDataLinks: null, + whyNoData: null, + dataLinks: null, + preregLinks: null, + preregLinkInfo: null, + hasPreregLinks: null, + dateWithdrawn: null, doi: null, @@ -133,12 +146,15 @@ export default Factory.extend({ }, }); + const providerId = preprint.id + ':osfstorage'; + const osfstorage = server.create('file-provider', { id: providerId, target: preprint }); + preprint.update({ contributors: allContributors, bibliographicContributors: allContributors, license, subjects, - files: [file], + files: [osfstorage], primaryFile: file, node, }); @@ -155,11 +171,14 @@ export default Factory.extend({ isContributor: trait({ afterCreate(preprint, server) { - const { currentUserId } = server.schema.roots.first(); - server.create('contributor', { + const contributors = preprint.contributors.models; + const firstContributor = server.create('contributor', { preprint, - id: currentUserId, + index:0, + users: server.schema.roots.first().currentUser as ModelInstance, }); + contributors.splice(0,1,firstContributor); + preprint.update({ contributors, bibliographicContributors:contributors }); }, }), @@ -203,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/fixtures/preprint-providers.ts b/mirage/fixtures/preprint-providers.ts index e15de80a363..46bc0029762 100644 --- a/mirage/fixtures/preprint-providers.ts +++ b/mirage/fixtures/preprint-providers.ts @@ -21,7 +21,9 @@ const preprintProviders: Array> = [ assets: randomAssets(1), footerLinks: 'fake footer links', reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION, + assertionsEnabled: true, allowCommenting: true, + allowSubmissions: true, }, { id: 'thesiscommons', @@ -31,6 +33,7 @@ const preprintProviders: Array> = [ assets: randomAssets(2), // eslint-disable-next-line max-len footerLinks: '

    LawArXiv: Support Contact  

    \n

    LawrXiv is a trademark of Cornell University, used under license. This license should not be understood to indicate endorsement of content on LawArXiv by Cornell University or arXiv.

    ', + allowSubmissions: true, }, { id: 'preprintrxiv', @@ -42,6 +45,7 @@ const preprintProviders: Array> = [ footerLinks: 'Removed in mirage scenario', reviewsCommentsPrivate: true, reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION, + allowSubmissions: true, }, { id: 'paperxiv', @@ -51,6 +55,7 @@ const preprintProviders: Array> = [ assets: randomAssets(4), // eslint-disable-next-line max-len footerLinks: '

    AgriXiv: Support Contact      

    arXiv is a trademark of Cornell University, used under license. This license should not be understood to indicate endorsement of content on AgriXiv by Cornell University or arXiv.

    ', + allowSubmissions: true, }, { id: 'thesisrxiv', @@ -60,6 +65,7 @@ const preprintProviders: Array> = [ assets: randomAssets(5), // eslint-disable-next-line max-len footerLinks: '

    AgriXiv: Support Contact      

    arXiv is a trademark of Cornell University, used under license. This license should not be understood to indicate endorsement of content on AgriXiv by Cornell University or arXiv.

    ', + allowSubmissions: true, }, { id: 'workrxiv', @@ -68,6 +74,7 @@ const preprintProviders: Array> = [ preprintWord: 'work', assets: randomAssets(6), footerLinks: 'fake footer links', + allowSubmissions: true, }, { id: 'docrxiv', @@ -76,6 +83,7 @@ const preprintProviders: Array> = [ preprintWord: 'default', assets: randomAssets(7), footerLinks: 'fake footer links', + allowSubmissions: true, }, { id: 'agrixiv', @@ -84,6 +92,7 @@ const preprintProviders: Array> = [ preprintWord: 'preprint', assets: randomAssets(8), reviewsWorkflow: PreprintProviderReviewsWorkFlow.POST_MODERATION, + allowSubmissions: true, }, { id: 'biohackrxiv', @@ -91,6 +100,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(9), + allowSubmissions: true, }, { id: 'nutrixiv', @@ -98,6 +108,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10), + allowSubmissions: true, }, { id: 'paleorxiv', @@ -105,6 +116,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10, false), + allowSubmissions: true, }, { id: 'sportrxiv', @@ -112,6 +124,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'paper', assets: randomAssets(10), + allowSubmissions: true, }, ]; diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index e6b54c9748c..db16a251b85 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -14,6 +14,8 @@ import { registrationFullScenario as registrationsFullScenario } from './registr 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, @@ -46,6 +48,9 @@ export default function(server: Server) { ]; const currentUser = server.create('user', ...userTraits); + // Add a bunch of users + userScenario(server); + // Optional Scenarios if (mirageScenarios.includes('dashboard')) { dashboardScenario(server, currentUser); @@ -75,7 +80,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 00000000000..128e068f041 --- /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 440c060be20..670bd1d9d04 100644 --- a/mirage/scenarios/preprints.ts +++ b/mirage/scenarios/preprints.ts @@ -1,6 +1,10 @@ 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 { + PreprintDataLinksEnum, + PreprintPreregLinksEnum, + PreprintPreregLinkInfoEnum, +} from 'ember-osf-web/models/preprint'; import PreprintProvider from 'ember-osf-web/models/preprint-provider'; import { ReviewsState } from 'ember-osf-web/models/provider'; @@ -54,16 +58,23 @@ function buildOSF( 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 osfApprovedAdminIdentifier = server.create('identifier'); approvedAdminPreprint.update({ identifiers: [osfApprovedAdminIdentifier] }); - const notContributorPreprint = server.create('preprint', { + const notContributorPreprint = server.create('preprint', Object({ provider: osf, id: 'osf-not-contributor', title: 'Preprint RWF: Pre-moderation, Non-Admin and Rejected', @@ -74,7 +85,8 @@ function buildOSF( whyNoData: `Why No Data\n${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, whyNoPrereg: `Why No Prereg\n${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, tags: [], - }); + isPreprintDoi: false, + })); const rejectedPreprint = server.create('preprint', { provider: osf, @@ -102,6 +114,7 @@ function buildOSF( hasCoi: true, hasDataLinks: PreprintDataLinksEnum.AVAILABLE, dataLinks: ['Data link 1', 'Data link 2', 'Data link 3'], + preregLinkInfo: PreprintPreregLinkInfoEnum.PREREG_ANALYSIS, hasPreregLinks: PreprintPreregLinksEnum.AVAILABLE, preregLinks: ['Prereg link 1', 'Prereg link 2', 'Prereg link 3'], conflictOfInterestStatement: `${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, @@ -226,6 +239,8 @@ function buildOSF( 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
    ', @@ -296,6 +311,8 @@ function buildrXiv( preprintrxiv.update({ allowSubmissions: true, highlightedSubjects: subjects, + subjects, + licensesAcceptable: server.schema.licenses.all(), // 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: '', @@ -313,7 +330,9 @@ function buildThesisCommons( currentUser: ModelInstance, ) { const thesisCommons = server.schema.preprintProviders.find('thesiscommons') as ModelInstance; + const brand = server.create('brand', { + primaryColor: '#821e1e', secondaryColor: '#94918e', heroBackgroundImage: 'https://singlecolorimage.com/get/94918e/1000x1000', @@ -329,7 +348,9 @@ function buildThesisCommons( thesisCommons.update({ highlightedSubjects: subjects, + subjects, brand, + licensesAcceptable: server.schema.licenses.all(), moderators: [currentUserModerator], preprints, description: '

    This is the description for Thesis Commons and it has an inline-style!

    ', @@ -376,6 +397,7 @@ function buildAgrixiv( agrixiv.update({ moderators: [currentUserModerator], + licensesAcceptable: server.schema.licenses.all(), brand: agrixivBrand, description: '

    This is the description for agrixiv!

    ', preprints: [ @@ -396,6 +418,7 @@ function buildNutrixiv( }); nutrixiv.update({ brand: nutrixivBrand, + licensesAcceptable: server.schema.licenses.all(), description: '

    This is the description for nutrixiv!

    ', }); } @@ -421,6 +444,7 @@ function buildBiohackrxiv(server: Server) { biohackrxiv.update({ brand: biohackrxivBrand, + licensesAcceptable: server.schema.licenses.all(), description: '

    This is the description for biohackrxiv!

    ', preprints: [publicDoiPreprint], }); diff --git a/mirage/scenarios/user.ts b/mirage/scenarios/user.ts new file mode 100644 index 00000000000..4914c322d22 --- /dev/null +++ b/mirage/scenarios/user.ts @@ -0,0 +1,31 @@ +import { Server } from 'ember-cli-mirage'; + + +export function userScenario(server: Server) { + server.create('user', { + givenName: 'Tom', + familyName: 'Brady', + }); + + for(let i = 1; i < 20; i++) { + server.create('user', { + givenName: 'Tom', + familyName: `Brady - ${i}`, + }); + } + + server.create('user', { + givenName: 'Harry', + familyName: 'Bailey', + }); + + server.create('user', { + givenName: 'George', + familyName: 'Bailey', + }); + + server.create('user', { + givenName: 'Taylor', + familyName: 'Swift', + }); +} diff --git a/mirage/serializers/contributor.ts b/mirage/serializers/contributor.ts index 8cc83825bdd..a7d4e34923c 100644 --- a/mirage/serializers/contributor.ts +++ b/mirage/serializers/contributor.ts @@ -31,6 +31,17 @@ export default class ContributorSerializer extends ApplicationSerializer) { const relationships: SerializedRelationships = {}; + if (model.preprint !== null) { + const { preprint } = model; + relationships.preprint = { + links: { + related: { + href: `${apiUrl}/v2/preprints/${preprint.id}`, + meta: this.buildRelatedLinkMeta(model, 'preprint'), + }, + }, + }; + } if (model.node !== null) { const { node } = model; relationships.node = { diff --git a/mirage/serializers/file-provider.ts b/mirage/serializers/file-provider.ts index 4bfa2fa425c..fc3fa4eefb5 100644 --- a/mirage/serializers/file-provider.ts +++ b/mirage/serializers/file-provider.ts @@ -59,10 +59,20 @@ export default class FileSerializer extends ApplicationSerializer) { const pathName = pluralize(underscore(model.targetId.type)); - return { - ...super.buildNormalLinks(model), + let links = { upload: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.name}/upload`, new_folder: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.name}/upload/?kind=folder`, }; + if(pathName === 'preprints') { + links = { + upload: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.id}/upload`, + new_folder: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.id}/upload/?kind=folder`, + }; + } + + return { + ...super.buildNormalLinks(model), + ...links, + }; } } diff --git a/mirage/serializers/node.ts b/mirage/serializers/node.ts index f40e5ab6ccd..94210ecee73 100644 --- a/mirage/serializers/node.ts +++ b/mirage/serializers/node.ts @@ -141,6 +141,22 @@ export default class NodeSerializer extends ApplicationSerializer { }, }, }, + subjects: { + links: { + related: { + href: `${apiUrl}/v2/nodes/${model.id}/subjects/`, + meta: this.buildRelatedLinkMeta(model, 'subjects'), + }, + }, + }, + subjectsAcceptable: { + links: { + related: { + href: `${apiUrl}/v2/nodes/${model.id}/subjectsAcceptable/`, + meta: this.buildRelatedLinkMeta(model, 'subjectsAcceptable'), + }, + }, + }, }; if (model.attrs.parentId !== null) { const { parentId } = model.attrs; diff --git a/mirage/serializers/preprint.ts b/mirage/serializers/preprint.ts index 46a883b6e18..7e35ba3b294 100644 --- a/mirage/serializers/preprint.ts +++ b/mirage/serializers/preprint.ts @@ -8,7 +8,7 @@ const { OSF: { apiUrl } } = config; export default class PreprintSerializer extends ApplicationSerializer { buildNormalLinks(model: ModelInstance) { return { - self: `${apiUrl}/v2/${model.id}/`, + self: `${apiUrl}/v2/preprints/${model.id}/`, doi: model.doi ? `https://doi.org/${model.doi}` : null, preprint_doi: model.isPreprintDoi ? `https://doi.org/10.31219/osf.io/${model.id}` : null, }; @@ -16,7 +16,26 @@ export default class PreprintSerializer extends ApplicationSerializer) { const relationships: SerializedRelationships = { - provider: { + 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 = { data: { id: model.provider.id, type: 'preprint-providers', @@ -27,92 +46,107 @@ export default class PreprintSerializer extends ApplicationSerializer, @@ -42,3 +43,40 @@ export function addContributor(this: HandlerContext, schema: Schema, request: Re } return contributorCreated; } + +export function addPreprintContributor(this: HandlerContext, schema: Schema, request: Request) { + const attrs = this.normalizedRequestAttrs('contributor'); + const { preprintID } = request.params; + const preprint = schema.preprints.find(preprintID) as ModelInstance; + let contributorCreated; + + if (attrs.usersId) { + // The request comes with an id in the payload + // That means we are adding an existing OSFUser as a contributor + const user = schema.users.find(attrs.usersId); + contributorCreated = schema.contributors.create({ + id: `${preprintID}-${attrs.usersId}`, + permission: attrs.permission, + bibliographic: attrs.bibliographic, + users: user, + preprint, + }); + } else if (attrs.fullName) { + // The request comes without an id in the payload + // That means we are inviting a user as a contributor + const user = schema.users.create({ fullName: attrs.fullName }); + contributorCreated = schema.contributors.create({ + id: `${preprintID}-${user.id}`, + permission: attrs.permission, + bibliographic: attrs.bibliographic, + users: user, + preprint, + }); + } + + if (contributorCreated!.bibliographic) { + preprint.bibliographicContributors.models.pushObject(contributorCreated!); + preprint.save(); + } + return contributorCreated; +} diff --git a/mirage/views/file.ts b/mirage/views/file.ts index f36fab414cb..60ed1aa2e98 100644 --- a/mirage/views/file.ts +++ b/mirage/views/file.ts @@ -2,6 +2,7 @@ import { HandlerContext, ModelInstance, Response, Schema } from 'ember-cli-mirag import { MirageNode } from 'ember-osf-web/mirage/factories/node'; import DraftNode from 'ember-osf-web/models/draft-node'; import { FileItemKinds } from 'ember-osf-web/models/base-file-item'; +import PreprintModel from 'ember-osf-web/models/preprint'; import faker from 'faker'; import { guid } from '../factories/utils'; @@ -46,9 +47,14 @@ export function uploadToRoot(this: HandlerContext, schema: Schema) { const uploadAttrs = this.request.requestBody; const { parentID, fileProviderId } = this.request.params; const { name, kind } = this.request.queryParams; - let node; + let isPreprint = false; + let node: any | PreprintModel; + if (this.request.url.includes('draft_nodes')) { node = schema.draftNodes.find(parentID); + } else if (this.request.url.includes('preprints')) { + isPreprint = true; + node = schema.preprints.find(parentID) as ModelInstance; } else { node = schema.nodes.find(parentID); if (node.storage && node.storage.isOverStorageCap) { @@ -57,8 +63,16 @@ export function uploadToRoot(this: HandlerContext, schema: Schema) { }); } } - const fileProvider = schema.fileProviders.findBy({ providerId: `${node.id}:${fileProviderId}` }); + + let fileProvider; + if (isPreprint) { + fileProvider = schema.fileProviders.findBy({ id: `${fileProviderId}` }); + } else { + fileProvider = schema.fileProviders.findBy({ providerId: `${node.id}:${fileProviderId}` }); + } + const { rootFolder } = fileProvider; + const randomNum = faker.random.number(); const fileGuid = guid('file'); const id = fileGuid(randomNum); @@ -83,8 +97,21 @@ export function uploadToRoot(this: HandlerContext, schema: Schema) { uploadedFile.size = uploadAttrs.size; } - rootFolder.files.models.pushObject(uploadedFile); - rootFolder.save(); + fileProvider.files.models.pushObject(uploadedFile); + fileProvider.save(); + + /* + if (isPreprint) { + /* eslint-disable-next-line * / + // node.primaryFile = uploadedFile; + node.save(); + } + */ + + if (rootFolder) { + rootFolder.files.models.pushObject(uploadedFile); + rootFolder.save(); + } return uploadedFile; } diff --git a/mirage/views/preprint.ts b/mirage/views/preprint.ts new file mode 100644 index 00000000000..dcc8363c805 --- /dev/null +++ b/mirage/views/preprint.ts @@ -0,0 +1,83 @@ +import { HandlerContext, ModelInstance, Request, Response, Schema } from 'ember-cli-mirage'; +import { Permission } from 'ember-osf-web/models/osf-model'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import faker from 'faker'; + +import { guid } from '../factories/utils'; + + +export function createPreprint(this: HandlerContext, schema: Schema) { + const now = new Date(); + const randomNum = faker.random.number(); + const preprintGuid = guid('preprint'); + const id = preprintGuid(randomNum); + + const attrs = { + ...this.normalizedRequestAttrs('preprint'), + id, + dateModified: now, + dateCreated: now, + isPublished: false, + originalPublicationDate: null, + dateLastTransitioned: null, + hasCoi: null, + conflictOfInterestStatement: null, + hasDataLinks: null, + whyNoData: null, + dataLinks: null, + preregLinks: null, + preregLinkInfo: null, + hasPreregLinks: null, + doi: null, + dateWithdrawn: null, + public: false, + citation: null, + subjects: [], + tags: [] as string[] , + currentUserPermission: [Permission.Admin, Permission.Read, Permission.Write], + }; + const preprint = schema.preprints.create(attrs) as ModelInstance; + + + const userId = schema.roots.first().currentUserId; + + if (userId) { + const currentUser = schema.users.find(userId); + const contributor = schema.contributors.create({ + preprint, + users: currentUser, + index: 0, + permission: Permission.Admin, + bibliographic: true, + }); + + preprint.bibliographicContributors.models.push(contributor); + preprint.save(); + } + + const providerId = preprint.id + ':osfstorage'; + schema.fileProviders.create({ id: providerId, target: preprint }); + preprint.save(); + + return preprint; +} + +export function updatePreprint(this: HandlerContext, schema: Schema, request: Request) { + const resource = schema.resources.find(request.params.id); + const attributes = { + ...this.normalizedRequestAttrs('resource'), + }; + if ('pid' in attributes) { + if (!attributes.pid || !attributes.pid.startsWith('10.')) { + return new Response(400, {}, { + errors: [{ + status: '400', + detail: 'invalid doi', + source: {pointer: '/data/attributes/pid'}, + }], + }); + } + } + resource.update(attributes); + return this.serialize(resource); +} diff --git a/mirage/views/provider-subjects.ts b/mirage/views/provider-subjects.ts index 3a6e79f1f9a..b092f045df5 100644 --- a/mirage/views/provider-subjects.ts +++ b/mirage/views/provider-subjects.ts @@ -1,8 +1,9 @@ import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage'; import Subject from 'ember-osf-web/models/subject'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; import { process } from './utils'; -function getFilterOpts( +export function getFilterOpts( queryParams: { [key: string]: string }, ): { type: string, value: string } { if ('filter[parent]' in queryParams) { @@ -46,3 +47,38 @@ export function getProviderSubjects(this: HandlerContext, schema: Schema, reques { defaultPageSize: Number(pageSize) }, ); } + +export function getPreprintProviderSubjects(this: HandlerContext, schema: Schema, request: Request) { + const { parentID: providerId } = request.params; + const { pageSize } = request.queryParams; + const filterOpts = getFilterOpts(request.queryParams); + + const provider = schema.preprintProviders.find(providerId) as ModelInstance; + const subjects = provider.subjects.models; + let filteredSubjects: Array>; + + if (filterOpts.type === 'parent') { + if (filterOpts.value === 'null') { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => !subject.parent, + ); + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.parent && (subject.parent.id === filterOpts.value), + ); + } + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.text.includes(filterOpts.value), + ); + } + + return process( + schema, + request, + this, + filteredSubjects.map(subject => this.serialize(subject).data), + { defaultPageSize: Number(pageSize) }, + ); +} + diff --git a/mirage/views/search.ts b/mirage/views/search.ts index 36fafeab240..e26a1e9369d 100644 --- a/mirage/views/search.ts +++ b/mirage/views/search.ts @@ -469,7 +469,7 @@ export function valueSearch(_: Schema, __: Request) { matchingHighlight: 'National Institute of Health', }, ], - cardSearchResultCount: 2134, + cardSearchResultCount: 3, }, relationships: { indexCard: { diff --git a/mirage/views/subjects-acceptable.ts b/mirage/views/subjects-acceptable.ts new file mode 100644 index 00000000000..2e8ec79ee74 --- /dev/null +++ b/mirage/views/subjects-acceptable.ts @@ -0,0 +1,36 @@ +import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage'; +import Subject from 'ember-osf-web/models/subject'; +import { process } from './utils'; +import { getFilterOpts } from './provider-subjects'; + +export function getSubjectsAcceptable(this: HandlerContext, schema: Schema, request: Request) { + const { pageSize } = request.queryParams; + const filterOpts = getFilterOpts(request.queryParams); + + const subjects = schema.subjects.all().models; + let filteredSubjects: Array>; + + if (filterOpts.type === 'parent') { + if (filterOpts.value === 'null') { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => !subject.parent, + ); + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.parent && (subject.parent.id === filterOpts.value), + ); + } + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.text.includes(filterOpts.value), + ); + } + + return process( + schema, + request, + this, + filteredSubjects.map(subject => this.serialize(subject).data), + { defaultPageSize: Number(pageSize) }, + ); +} diff --git a/mirage/views/user.ts b/mirage/views/user.ts index cadcfa1e04c..fd8924ad8aa 100644 --- a/mirage/views/user.ts +++ b/mirage/views/user.ts @@ -30,6 +30,23 @@ export function userRegistrationList(this: HandlerContext, schema: Schema, reque return json; } +export function userPreprintList(this: HandlerContext, schema: Schema, request: Request) { + const user = schema.users.find(request.params.id); + const preprints = []; + const { contributorIds } = user; + + for (const contributorId of contributorIds as string[]) { + const contributor = schema.contributors.find(contributorId); + const preprint = contributor.preprint; + if (preprint && filter(preprint, request)) { + preprints.push(this.serialize(preprint).data); + } + } + + const json = process(schema, request, this, preprints, { defaultSortKey: 'last_logged' }); + return json; +} + export function claimUnregisteredUser(this: HandlerContext) { return new Response(204); } diff --git a/mirage/views/utils/-private.ts b/mirage/views/utils/-private.ts index efaebd54777..23a6ceff0bf 100644 --- a/mirage/views/utils/-private.ts +++ b/mirage/views/utils/-private.ts @@ -195,21 +195,24 @@ export function compareStrings( comparisonValue: any, operator: ComparisonOperators, ): boolean { + const lowerCaseActualValue = actualValue.toLowerCase(); if (comparisonValue instanceof Array) { switch (operator) { case ComparisonOperators.Eq: - return comparisonValue.some(element => actualValue.includes(element)); + return comparisonValue.some(element => lowerCaseActualValue.includes(element.toLowerCase())); case ComparisonOperators.Ne: - return comparisonValue.every(element => !actualValue.includes(element)); + return comparisonValue.every(element => !lowerCaseActualValue.includes(element.toLowerCase())); default: throw new Error(`String arrays can't be compared with "${operator}".`); } } else { + const lowerCaseComparisonlValue = comparisonValue.toLowerCase(); switch (operator) { case ComparisonOperators.Eq: - return actualValue.includes(comparisonValue); + + return lowerCaseActualValue.includes(lowerCaseComparisonlValue); case ComparisonOperators.Ne: - return !actualValue.includes(comparisonValue); + return !lowerCaseActualValue.includes(lowerCaseComparisonlValue); default: throw new Error(`Strings can't be compared with "${operator}".`); } diff --git a/package.json b/package.json index f2a7305e783..7233bf3c9c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.04.0", + "version": "24.07.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", diff --git a/public/assets/images/preprints/bg-light.jpg b/public/assets/images/preprints/bg-light.jpg new file mode 100644 index 00000000000..7fd34b9124a Binary files /dev/null and b/public/assets/images/preprints/bg-light.jpg differ diff --git a/tests/acceptance/guid-node/metadata-test.ts b/tests/acceptance/guid-node/metadata-test.ts index 5bb355df6b0..5d76a040d5c 100644 --- a/tests/acceptance/guid-node/metadata-test.ts +++ b/tests/acceptance/guid-node/metadata-test.ts @@ -45,7 +45,7 @@ module('Acceptance | guid-node/metadata', hooks => { .containsText(funder.award_number, `Funder award number is correct for ${funder.funder_name}`); } assert.dom('[data-test-contributors-list]').exists(); - assert.dom('[data-test-subjects-list]').doesNotExist('There are no subjects for projects'); + assert.dom('[data-test-subjects-list]').exists('Subjects list is displayed for projects'); assert.dom('[data-test-edit-node-description-button]').doesNotExist(); assert.dom('[data-test-edit-resource-metadata-button]').doesNotExist(); @@ -86,7 +86,7 @@ module('Acceptance | guid-node/metadata', hooks => { .doesNotExist(`Funder award number does not exist for ${funder.funder_name}`); } assert.dom('[data-test-contributors-list]').doesNotExist('There are no contributors for AVOL'); - assert.dom('[data-test-subjects-list]').doesNotExist('There are no subjects for projects'); + assert.dom('[data-test-subjects-list]').exists('Subjects list is displayed for projects'); assert.dom('[data-test-edit-node-description-button]').doesNotExist(); assert.dom('[data-test-edit-resource-metadata-button]').doesNotExist(); @@ -126,7 +126,7 @@ module('Acceptance | guid-node/metadata', hooks => { .containsText(funder.award_number, `Funder award number is correct for ${funder.funder_name}`); } assert.dom('[data-test-contributors-list]').exists(); - assert.dom('[data-test-subjects-list]').doesNotExist('There are no subjects for projects'); + assert.dom('[data-test-subjects-list]').exists('Subjects list is displayed for projects'); assert.dom('[data-test-edit-node-description-button]').exists(); await click('[data-test-edit-node-description-button]'); diff --git a/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts b/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts index e1ba4f6d316..923dad6bf1b 100644 --- a/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts +++ b/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts @@ -42,7 +42,7 @@ module('Registries | Acceptance | branded.moderation | moderators', hooks => { 'On the moderators page of registries reviews'); assert.dom('[data-test-moderator-row]').exists({ count: 4 }, 'There are 4 moderators shown'); assert.dom('[data-test-delete-moderator-button]') - .exists({ count: 1 }, 'Only one moderator is able to be removed'); + .exists({ count: 2 }, 'Only one moderator is able to be removed'); assert.dom('[data-test-moderator-permission-option]') .doesNotExist('Moderators are not able to edit permissions'); assert.dom(`[data-test-delete-moderator-button=${currentUser.id}]`).exists('Only able to remove self'); @@ -64,7 +64,7 @@ module('Registries | Acceptance | branded.moderation | moderators', hooks => { assert.dom('[data-test-moderator-permission-option]') .exists({ count: 4 }, 'Admins are able to edit permissions for all users'); assert.dom('[data-test-delete-moderator-button]') - .exists({ count: 4 }, 'All moderators and admins are able to be removed'); + .exists({ count: 8 }, 'All moderators and admins are able to be removed'); assert.dom('[data-test-add-moderator-button]') .exists('Button to add moderator is visible for admins'); }); diff --git a/tests/integration/components/contributors/component-test.ts b/tests/integration/components/contributors/component-test.ts index 69c1a29d837..378f0826182 100644 --- a/tests/integration/components/contributors/component-test.ts +++ b/tests/integration/components/contributors/component-test.ts @@ -69,7 +69,6 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-contributor-card]').exists(); assert.dom('[data-test-contributor-card-main]').exists(); - assert.dom('[data-test-contributor-gravatar]').exists(); assert.dom(`[data-test-contributor-link="${contributor.id}"]`) .hasText(contributor.users.get('fullName')); assert.dom(`[data-test-contributor-permission="${contributor.id}"]`) @@ -97,7 +96,6 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-contributor-card]').exists(); assert.dom('[data-test-contributor-card-main]').exists(); - assert.dom('[data-test-contributor-gravatar]').exists(); assert.dom('[data-test-contributor-link]').doesNotExist(); assert.dom('[data-test-contributor-card-main]') .containsText(unregContributor.unregisteredContributor!); @@ -172,7 +170,6 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-contributor-card]').exists(); assert.dom('[data-test-contributor-card-main]').exists(); - assert.dom('[data-test-contributor-gravatar]').exists(); assert.dom(`[data-test-contributor-link="${contributor.id}"]`) .hasText(contributor.users.fullName); assert.dom(`[data-test-contributor-permission="${contributor.id}"]`) @@ -321,7 +318,7 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-user-search-input]').exists('User serach button renders'); assert.dom('[data-test-add-unregistered-contributor-button]').exists('Add unregistered contrib button renders'); - assert.dom('[data-test-user-search-results]').exists('Search result continer renders'); + assert.dom('[data-test-user-search-results]').doesNotExist('Search result container does not exist'); assert.dom('[data-test-contributor-card]').doesNotExist('No contributors are on the draft'); await fillIn('[data-test-user-search-input]', 'Bae'); await click('[data-test-user-search-button]'); diff --git a/tests/integration/components/moderators/component-test.ts b/tests/integration/components/moderators/component-test.ts index a1351c879ff..d1b36697b09 100644 --- a/tests/integration/components/moderators/component-test.ts +++ b/tests/integration/components/moderators/component-test.ts @@ -83,7 +83,7 @@ module('Integration | Component | moderators', hooks => { ); assert.dom('[data-test-moderator-link]').exists({ count: 2 }); assert.dom('[data-test-permission-group]').exists({ count: 2 }); - assert.dom('[data-test-delete-moderator-button]').exists({ count: 2 }); + assert.dom('[data-test-delete-moderator-button]').exists({ count: 4 }); assert.dom(`[data-test-moderator-row="${currentUser.id}"]>div>[data-test-permission-group]`).hasText('Admin'); assert.dom(`[data-test-moderator-row="${moderator.id}"]>div>[data-test-permission-group]`).hasText('Moderator'); await clickTrigger(`[data-test-moderator-row="${moderator.id}"]`); @@ -97,7 +97,7 @@ module('Integration | Component | moderators', hooks => { await click('[data-test-confirm-delete]'); assert.dom('[data-test-moderator-link]').exists({ count: 1 }); assert.dom('[data-test-permission-group]').exists({ count: 1 }); - assert.dom('[data-test-delete-moderator-button]').exists({ count: 1 }); + assert.dom('[data-test-delete-moderator-button]').exists({ count: 2 }); }); test('can only remove self as a moderator', async function( @@ -130,7 +130,7 @@ module('Integration | Component | moderators', hooks => { assert.dom('[data-test-add-moderator-button').doesNotExist(); assert.dom('[data-test-moderator-link]').exists({ count: 2 }); assert.dom('[data-test-permission-group]').exists({ count: 2 }); - assert.dom('[data-test-delete-moderator-button]').exists({ count: 1 }); + assert.dom('[data-test-delete-moderator-button]').exists({ count: 2 }); assert.dom(`[data-test-delete-moderator-button="${admin.id}"]`).doesNotExist(); assert.dom( `[data-test-moderator-row="${currentUser.id}"]>div>[data-test-permission-group]`, diff --git a/tests/integration/components/preprint-affiliated-institutions/component-test.ts b/tests/integration/components/preprint-affiliated-institutions/component-test.ts new file mode 100644 index 00000000000..8da970982a3 --- /dev/null +++ b/tests/integration/components/preprint-affiliated-institutions/component-test.ts @@ -0,0 +1,65 @@ +import { 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 { ModelInstance } from 'ember-cli-mirage'; + + +module('Integration | Component | PreprintAffiliatedInstitutions', hooks => { + 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/tests/integration/helpers/is-mobile-test.ts b/tests/integration/helpers/is-mobile-test.ts new file mode 100644 index 00000000000..05cf4b34015 --- /dev/null +++ b/tests/integration/helpers/is-mobile-test.ts @@ -0,0 +1,24 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +import { setBreakpoint } from 'ember-responsive/test-support'; + +module('Integration | Helper | is-mobile', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + setBreakpoint('mobile'); + + await render(hbs` + {{!-- template-lint-disable block-indentation --}} +
    {{if (is-mobile) 'mobile' 'not-mobile'}}
    + `); + + assert.dom('[data-test-div]').hasText('mobile'); + + setBreakpoint('desktop'); + assert.dom('[data-test-div]').hasText('not-mobile'); + }); +}); diff --git a/tests/unit/models/review-action-test.ts b/tests/unit/models/review-action-test.ts index 0e184831582..35cfdead54e 100644 --- a/tests/unit/models/review-action-test.ts +++ b/tests/unit/models/review-action-test.ts @@ -27,7 +27,7 @@ module('Unit | Model | review-action', hooks => { const relationship = get(model, 'relationshipsByName').get('target'); assert.equal(relationship.key, 'target'); - assert.equal(relationship.type, 'registration'); + assert.equal(relationship.type, 'abstract-node'); assert.equal(relationship.kind, 'belongsTo'); }); diff --git a/tests/unit/validators/url-with-protocol-test.js b/tests/unit/validators/url-with-protocol-test.js new file mode 100644 index 00000000000..eebd6b40fdc --- /dev/null +++ b/tests/unit/validators/url-with-protocol-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { validateUrlWithProtocols } from 'ember-osf-web/validators/url-with-protocol'; + +module('Unit | Validator | url-with-protocol', function(hooks) { + setupTest(hooks); + + test('it works', function(assert) { + const baseValidator = validateUrlWithProtocols(); + assert.ok(baseValidator('http://a.com'), 'http is valid'); + assert.ok(baseValidator('https://b.com'), 'https is valid'); + assert.strictEqual(baseValidator('c.com').type, 'url', 'no protocol is invalid'); + assert.strictEqual(baseValidator('http://d').type, 'url', 'no domain is invalid'); + assert.strictEqual(baseValidator('ftp://e.com').type, 'url', 'ftp is invalid'); + + const customValidator = validateUrlWithProtocols({ acceptedProtocols: ['http'] }); + assert.ok(customValidator('http://a.com'), 'http is valid'); + assert.strictEqual(customValidator('https://b.com').type, 'url', 'https is invalid'); + assert.strictEqual(customValidator('c.com').type, 'url', 'no protocol is invalid'); + assert.strictEqual(customValidator('http://d').type, 'url', 'no domain is invalid'); + assert.strictEqual(customValidator('ftp://e.com').type, 'url', 'ftp is invalid'); + }); +}); diff --git a/translations/en-us.yml b/translations/en-us.yml index 994744fc29e..613a3b2c2a5 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -24,19 +24,36 @@ documentType: pluralCapitalized: Theses singular: thesis singularCapitalized: Thesis -contact: +contact: email: support@osf.io general: - OSF: OSF - share: Share - embed: Embed + add: Add + and: and + api: API + apply: Apply + asc_paren: (asc) + available: 'Available' + back: Back + bookmark: Bookmark + cancel: Cancel + caution: Caution + close: Close + component: component + contributors: Contributors + copy: Copy + cos: 'Center for Open Science' + create: Create + date: Date + delete: Delete + desc_paren: (desc) + description: Description + done: Done download: Download download_url: 'Download url' - done: Done - delete: Delete - view: View + downloads: Downloads edit: Edit - cancel: Cancel + ellipsis: … + embed: Embed add: Add confirm: Confirm authorize: Authorize @@ -44,75 +61,66 @@ general: ok: OK apply: Apply revisions: Revisions + engineering: 'engineering' + filter: Filter + help: Help + home: Home + hosted_on_the_osf: 'Hosted on OSF' + last_modified: 'Last modified' + loading: Loading... md5: MD5 - date: Date - sha2: SHA2 - title: Title - contributors: Contributors modified: Modified - description: Description - create: Create - and: and - or: or - bookmark: Bookmark more: more - upload: Upload - rename: Rename - copy: Copy move: Move name: Name - size: Size - version: Version - downloads: Downloads - close: Close - back: Back - public: Public - filter: Filter - revert: Revert - save: Save - ellipsis: … - warning: Warning - caution: Caution - sort_asc: 'Sort ascending' - sort_desc: 'Sort descending' - last_modified: 'Last modified' - sort: Sort - asc_paren: (asc) - desc_paren: (desc) - loading: Loading... next: next - previous: previous - help: Help - api: API - cos: 'Center for Open Science' - home: Home + newFeaturePopoverHeading: 'New feature!' + newFeaturePopoverBody: 'You can now add funder information, resource types, and more enhanced metadata to your registration.' + no: 'No' + not-applicable: 'Not Applicable' + ok: OK + or: or + options: Options + optional: Optional + optional_paren: (optional) period: . - settings: Settings + please_confirm: 'Please confirm' + presented_by_osf: 'Presented by OSF' + previous: previous project: project - component: component + public: Public + OSF: OSF + other: Other registration: registration - hosted_on_the_osf: 'Hosted on OSF' - presented_by_osf: 'Presented by OSF' - please_confirm: 'Please confirm' + rename: Rename required: Required - options: Options - optional: Optional - optional_paren: (optional) - update: Update - user: User + revert: Revert + revisions: Revisions + save: Save + science: 'science' services: collections: Collections institutions: Institutions preprints: Preprints registries: Registries - other: Other + settings: Settings + sha2: SHA2 + share: Share + sort_asc: 'Sort ascending' + sort_desc: 'Sort descending' + sort: Sort + size: Size structured_data: json_ld_retrieval_error: 'Error retrieving JSON-LD object for Google Structured Data.' tags: 'Tags' - science: 'science' - engineering: 'engineering' - newFeaturePopoverHeading: 'New feature!' - newFeaturePopoverBody: 'You can now add funder information, resource types, and more enhanced metadata to your registration.' + title: Title + update: Update + upload: Upload + user: User + version: Version + view: View + warning: Warning + yes: 'Yes' file_actions_menu: actions: '{filename} actions' @@ -721,64 +729,64 @@ tos_consent: continue: Continue failed_save: 'Unable to save Terms of Services consent.' validationErrors: - description: 'This field' - inclusion: '{description} is not included in the list.' - exclusion: '{description} is reserved.' - invalid: '{description} is invalid.' - confirmation: '{description} doesn''t match {on}.' accepted: '{description} must be accepted.' - empty: 'This field can''t be empty.' + affirm_terms: 'You must read and agree to the Terms of Use and Privacy Policy.' + after: '{description} must be after {after}.' + before: '{description} must be before {before}.' blank: 'This field can''t be blank.' - present: '{description} must be blank.' collection: '{description} must be a collection.' - singular: '{description} can''t be a collection.' - tooLong: '{description} is too long (maximum is {max} characters).' - tooShort: '{description} is too short (minimum is {min} characters).' - before: '{description} must be before {before}.' - after: '{description} must be after {after}.' - wrongDateFormat: '{description} must be in the format of {format}.' - wrongLength: '{description} is the wrong length (should be {is} characters).' - notANumber: '{description} must be a number.' - notAnInteger: '{description} must be an integer.' - greaterThan: '{description} must be greater than {gt}.' - greaterThanOrEqualTo: '{description} must be greater than or equal to {gte}.' - equalTo: '{description} must be equal to {is}.' - lessThan: '{description} must be less than {lt}.' - lessThanOrEqualTo: '{description} must be less than or equal to {lte}.' - otherThan: '{description} must be other than {value}.' - odd: '{description} must be odd.' - even: '{description} must be even.' - positive: '{description} must be positive.' + confirmation: '{description} doesn''t match {on}.' date: '{description} must be a valid date.' - onOrAfter: '{description} must be on or after {onOrAfter}.' - onOrBefore: '{description} must be on or before {onOrBefore}.' + description: 'This field' email: '{description} must be a valid email address.' - phone: '{description} must be a valid phone number.' - url: '{description} must be a valid url.' - https_url: '{description} must be a valid https url.' - email_registered: 'This email address has already been registered.' + email_duplicate: 'Duplicate email' email_invalid: 'Invalid email address. If this should not have occurred, please report this to {supportEmail}' email_match: 'Email addresses must match.' - email_duplicate: 'Duplicate email' - password_email: 'Your password cannot be the same as your email address.' - password_old: 'Your new password cannot be the same as your old password.' - password_match: 'Passwords must match.' - recaptcha: 'Please complete reCAPTCHA.' - affirm_terms: 'You must read and agree to the Terms of Use and Privacy Policy.' - min_subjects: 'You must select at least one subject.' - node_license_invalid: 'Invalid required fields for the license' - node_license_missing_fields: 'The following required {numOfFields, plural, =1 {field is} other {fields are}} missing: {missingFields}' + email_registered: 'This email address has already been registered.' + empty: 'This field can''t be empty.' + equalTo: '{description} must be equal to {is}.' + even: '{description} must be even.' + exclusion: '{description} is reserved.' + greaterThan: '{description} must be greater than {gt}.' + greaterThanOrEqualTo: '{description} must be at least {gte}.' + https_url: '{description} must be a valid https url.' + inclusion: '{description} is not included in the list.' + invalid: '{description} is invalid.' invalid_doi: 'Please use a valid DOI format (10.xxxx/xxxxx)' + lessThan: '{description} must be less than {lt}.' + lessThanOrEqualTo: '{description} must be less than or equal to {lte}.' + license_not_accepted: 'Please select a license that is accepted by this collection.' + min_subjects: 'You must select at least one subject.' + missingFileNoProject: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found.' + moderator_comment: 'Please provide feedback for your decision.' mustSelect: 'You must select a value for this field.' - mustSelectMinOne: 'You must select at least one value for this field.' mustSelectFileMinOne: 'You must select at least one file for this field.' - missingFileNoProject: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found.' - onlyProjectOrComponentFiles: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found on this {projectOrComponent}.' + mustSelectMinOne: 'You must select at least one value for this field.' new_folder_name: 'Folder name must not be blank.' - year_format: 'Please specify a valid year.' no_updated_responses: 'No changes have been made in this update.' - moderator_comment: 'Please provide feedback for your decision.' - license_not_accepted: 'Please select a license that is accepted by this collection.' + node_license_invalid: 'Invalid required fields for the license' + node_license_missing_fields: 'The following required {numOfFields, plural, =1 {field is} other {fields are}} missing: {missingFields}' + notAnInteger: '{description} must be an integer.' + notANumber: '{description} must be a number.' + odd: '{description} must be odd.' + onlyProjectOrComponentFiles: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found on this {projectOrComponent}.' + onOrAfter: '{description} must be on or after {onOrAfter}.' + onOrBefore: '{description} must be on or before {onOrBefore}.' + otherThan: '{description} must be other than {value}.' + password_email: 'Your password cannot be the same as your email address.' + password_match: 'Passwords must match.' + password_old: 'Your new password cannot be the same as your old password.' + phone: '{description} must be a valid phone number.' + positive: '{description} must be positive.' + present: '{description} must be blank.' + recaptcha: 'Please complete reCAPTCHA.' + singular: '{description} can''t be a collection.' + tooLong: '{description} is too long (maximum is {max} characters).' + tooShort: '{description} is too short (minimum is {min} characters).' + url: '{description} must be a valid url.' + wrongDateFormat: '{description} must be in the format of {format}.' + wrongLength: '{description} is the wrong length (should be {is} characters).' + year_format: 'Please specify a valid year format (YYYY).' validated_input_form: discard_changes: 'Discard changes' node_navbar: @@ -884,6 +892,12 @@ node: contributors: Contributors add-ons: Add-ons settings: Settings + projects: + search-placeholder: 'Find project by name' + select-placeholder: 'Click to select project' + load-more: + loading: Loading… + load-more: 'Load More Projects' registrations: new_registration_modal: title: Register @@ -1302,41 +1316,187 @@ preprints: discover: title: 'Search' title: 'Preprints' + select: + page-title: 'Select Providers' + title: 'New Preprints' + select-button: 'Select' + deselect-button: 'Deselect' + heading: 'Select a preprint service' + 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: + title: 'Title and Abstract' + title-input: 'Title' + abstract-input: 'Abstract' + abstract-input-error: '20 characters' + step-file: + delete-modal-button: 'Continue' + delete-modal-button-tooltip: 'Version file' + delete-modal-title: 'Add a new {singularPreprintWord} file' + delete-warning: 'This will allow a new version of the {singularPreprintWord} file to be uploaded to the {singularPreprintWord}. The existing file will be retained as a version of the {singularPreprintWord}.' + file-select-label: 'Select from an existing OSF project' + file-select-help-text: 'Start a new {singularPreprintWord} to attach a file from your project.' + file-upload-label: 'Upload from your computer' + file-upload-help-text: 'Start a new {singularPreprintWord} to attach a file from your computer.' + file-upload-label-one: 'Drag and drop files here to upload' + file-upload-label-two: 'or click to browse for files.' + project-select-explanation: 'A file is attach to this {singularPreprintWord} draft. You can upload a new file version using the “Upload from your computer” option. Start a new {singularPreprintWord} if you need to attach a file from a project.' + title: 'File' + uploaded-file-title: 'Attached {singularPreprintWord} file' + upload-title: 'Upload your {singularPreprintWord}' + upload-warning: 'Note: You cannot switch options once a file is attached.' + step-metadata: + title: 'Metadata' + contributors-input: 'Contributors' + license-input: 'License' + license-description: 'A license tells others how they can use your work in the future and only applies to the information and files submitted with the registration. For more information, see this help guide.' + license-placeholder: 'Select one' + license-year-input: 'Copyright Year' + license-copyright-input: 'Copyright Holders' + subjects-input: 'Subjects' + tags-input: 'Tags' + 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' + conflict-of-interest-description: 'The Conflict of Interest (COI) assertion is made on behalf of all the authors listed for this preprint. COIs include: financial involvement in any entity such as honoraria, grants, speaking fees, employment, consultancies, stock ownership, expert testimony, and patents or licenses. COIs can also include non-financial interests such as personal or professional relationships or pre-existing beliefs in the subject matter or materials discussed in this preprint' + conflict-of-interest-placeholder: 'Describe' + conflict-of-interest-none: 'Author asserted there is no Conflict of Interest with this preprint.' + + public-link-add-button: 'Add another' + public-link-remove-button: 'Remove link' + + public-data-input: 'Public Data' + public-data-description: 'Data refers to raw and/or processed information (quantitative or qualitative) used for the analyses, case studies, and/or descriptive interpretation in the preprint. Public data could include data posted to open-access repositories, public archival library collection, or government archive. For data that is available under limited circumstances (e.g., after signing a data sharing agreement), choose the ‘No’ option and use the comment box to explain how others could access the data.' + public-data-link-placeholder: 'Link to data' + public-data-no-placeholder: 'Describe' + public-data-na-placeholder: 'Author asserted there is no data associated with this {singularPreprintWord}.' + + public-preregistration-input: 'Public Preregistration' + public-preregistration-description: ' + A preregistration is a description of the research design and/or analysis plan that is created and registered before researchers collected data or before they have seen/interacted with preexisting data. The description should appear in a public registry (e.g., clinicaltrials.gov, OSF, AEA registry).' + public-preregistration-link-placeholder: 'Link to preregistration' + public-preregistration-no-placeholder: 'Describe' + public-preregistration-na-placeholder: 'Author asserted there is no preregistration associated with this {singularPreprintWord}.' + + public-preregistration-link-info-placeholder: 'Choose one' + public-preregistration-link-info-designs: 'Study Design' + public-preregistration-link-info-analysis: 'Analysis Plan' + public-preregistration-link-info-both: 'Both' + step-supplements: + title: 'Supplements (Optional)' + description: 'Connect an OSF project to share data, code, protocols, or other supplemental materials.' + connect-button: 'Connect an existing OSF project' + choose-project: 'Choose project' + choose-project-line-one-description: 'This will make your project public, if it is not already.' + choose-project-line-two-description: 'The projects and components for which you have admin access are listed below.' + create-title: 'Create Project' + create-project-line-one-description: 'This creates a public project for your supplemental materials.' + create-project-line-two-description: 'Upload files and manage contributors on the project.' + project-title: 'New project title for supplemental materials.' + create-button: 'Create a new OSF project' + create-project: 'Create project' + delete-modal-title: 'Disconnect supplemental material' + delete-warning: 'This will disconnect the selected project. You can select new supplemental material or re-add the same supplemental material at a later date.' + step-review: + agreement-provider: '{providerName} uses {moderationType}. If your preprint is accepted, it will be assigned a DOI and become publicly accessible via OSF. The preprint file cannot be deleted but it can be updated or modified.' + agreement-provider-two: 'You can read more about OSF preprints moderation policies on the OSF support center.

    ' + agreement-title: 'Consent to publish' + agreement-user: 'By submitting this preprint you confirm that all contributors agree with sharing it and that you have the right to share this preprint.' + conflict-of-interest: 'Conflict of Interest' + contributors: 'Contributors' + no-conflict-of-interest: 'Author asserted no Conflict of Interest.' + preprint-service: '{singularPreprintWord} Service' + preprint-title: 'Title' + publication-citation: 'Publication Citation' + publication-date: 'Publication Date' + publication-doi: 'Publication DOI' + public-data: 'Public Data' + public-preregistration: 'Public Preregistration' + supplement-na: 'Author did not add any supplements for this {singularPreprintWord}' + supplement-title: 'OSF Project' + title: 'Review' + data-analytics: 'Goto {statusType} tab' + status-flow: + step-title-and-abstract: 'Title and Abstract' + step-file: 'File' + step-metadata: 'Metadata' + step-author-assertions: 'Author Assertions' + 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}' + error: 'Error saving {singularPreprintWord}.' + 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.' + 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' detail: abstract: 'Abstract' article_doi: 'Peer-reviewed Publication DOI' citations: 'Citations' collapse: 'Collapse' - date_label: + date_label: created_on: 'Created' submitted_on: 'Submitted' disciplines: 'Disciplines' expand: 'Expand' - header: + header: last_edited: 'Last edited' authors_label: 'Authors' withdrawn_on: 'Withdrawn' license: 'License' none: 'None' - original_publication_date: 'Original publication date' + publication-citation: 'Publication Citation' + original_publication_date: 'Original Publication Date' orphan_preprint: 'The user has removed this file.' preprint_doi: '{documentType} DOI' preprint_pending_doi: 'DOI created after {documentType} is made public' preprint_pending_doi_moderation: 'DOI created after moderator approval' 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: + project_button: edit_preprint: 'Edit {documentType}' edit_resubmit_preprint: 'Edit and resubmit' see_less: 'See less' see_more: 'See more' - share: + share: download: 'Download {documentType}' downloads: 'Downloads' download_file: 'Download file' 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' @@ -1346,7 +1506,7 @@ preprints: author-assertions: header_label: 'Author Assertions' describe: 'Describe' - available: + available: yes: 'Yes' no: 'No' available: 'Available' @@ -1377,10 +1537,10 @@ preprints: brand_name: 'OSF' loading: 'Loading...' close: 'Close' - message: + message: base: '{name} uses {reviewsWorkflow}. This {documentType}' pending_pre: 'is not publicly available or searchable until approved by a moderator.' - pending_post: 'is publicly available and searchable but is subject to removal by a moderator.' + pending_post: 'is publicly available and searchable but is subject to removal by a moderator.' accepted: 'has been accepted by a moderator and is publicly available and searchable.' rejected: 'has been rejected by a moderator and is not publicly available or searchable.' pending_withdrawal: 'This {documentType} has been requested by the authors to be withdrawn. It will still be publicly searchable until the request has been approved.' @@ -1391,7 +1551,7 @@ preprints: rejected: 'rejected' pending_withdrawal: 'pending withdrawal' withdrawal_rejected: 'withdrawal rejected' - feedback: + feedback: moderator_feedback: 'Moderator feedback' moderator: 'Moderator' base: 'This {documentType}' @@ -1419,15 +1579,35 @@ preprints: bottom: contact: 'Contact us' p1: 'Create your own branded {documentType} servers backed by the OSF.' - div: + div: line1: 'Check out the' linkText1: 'open source code' line2: 'and our' linkText2: 'public roadmap' line3: '. Input welcome!' - advisory: + advisory: heading: 'Advisory Group' paragraph: 'Our advisory group includes leaders in preprints and scholarly communication' + my_preprints: + header: 'My Preprints' + sorted: 'Sorted by last updated' + preprint_card: + statuses: + pending: 'Pending' + accepted: 'Accepted' + rejected: 'Rejected' + contributors: 'Contributors:' + description: 'Description' + private_tooltip: 'This preprint is private' + options: 'Options' + manage_contributors: 'Manage Contributors' + view_button: 'View' + update_button: 'Edit' + settings: 'Settings' + delete: 'Delete' + provider: 'Provider:' + date_created: 'Date Created:' + date_modified: 'Date Modified:' registries: header: osf_registrations: 'OSF Registrations' @@ -1709,12 +1889,11 @@ registries: add_new_button: 'Add new contributors' done_add_new_button: 'Done adding new contributors' results_heading: 'Results' - search_placeholder: 'Search by name or profile information' - help_text: '

    Search results will appear here. Click the + icon in each row to set permissions for that contributor.

    You can perform additional searches to add more contributors. Your selections will remain listed below until you click Save.

    ' + search_placeholder: 'Search by name' search: 'Search' - add_unregistered_contributor: 'Add unregistered contributor' + add_unregistered_contributor: 'Add by email address' error_loading: 'Error, could not load contributors' - no_results: 'No results found' + no-results: 'No results found' add_contributor_aria: 'Add contributor' save: 'Save' clear_all: 'Clear all' @@ -2657,13 +2836,14 @@ osf-components: reorderContributor: reorderContributor: 'Reorder contributor' success: 'Contributor order updated.' - dragHandle: '⇕' noEducation: 'No education history to show' noEmployment: 'No employment history to show' addContributor: success: 'Contributor added' errorHeading: 'Error adding contributor' - currentContributors: 'Current contributors' + addContributors: 'Add Contributors' + currentContributors: 'Contributors' + permission-warning: 'Warning: Changing your permissions will prevent you from editing your draft.' email: 'Email' fullName: 'Full name' selectPermission: 'Select permission' @@ -2677,6 +2857,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' diff --git a/types/ember-changeset-validations/utils/validation-errors.d.ts b/types/ember-changeset-validations/utils/validation-errors.d.ts index bfb9d3f6d9b..19d53165256 100644 --- a/types/ember-changeset-validations/utils/validation-errors.d.ts +++ b/types/ember-changeset-validations/utils/validation-errors.d.ts @@ -11,4 +11,4 @@ export interface RawValidationResult extends ValidationResult { message: string; } -export default function buildMessage(key: tring, result: ValidationResult): string | RawValidationResult; +export default function buildMessage(key: string, result: ValidationResult): string | RawValidationResult;