From e949bd9b83f3553317c88782029f95d878e0f311 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:20:16 -0400 Subject: [PATCH 01/35] Add Editable Subjects Widget to Project Metadata Editor (#2227) - Ticket: [ENG-4354] - Feature flag: n/a ## Purpose To integrate an editable subjects widget into the project metadata editor ## Summary of Changes - Added subjectsAcceptable property to the Node model for project-specific subjects. - Updated SubjectManagerComponent to handle nodes without providers. - Modified BrowseManagerComponent to load subjectsAcceptable if the provider is not present. - Integrated the subjects widget into the node metadata form template for projects. --- app/models/node.ts | 2 + .../node-metadata-form/template.hbs | 34 ++++++++-------- .../browse/browse-manager/component.ts | 39 +++++++++++++------ .../components/subjects/manager/component.ts | 16 ++++++-- .../components/subjects/manager/template.hbs | 1 + mirage/config.ts | 2 + mirage/serializers/node.ts | 16 ++++++++ mirage/views/provider-subjects.ts | 2 +- mirage/views/subjects-acceptable.ts | 36 +++++++++++++++++ tests/acceptance/guid-node/metadata-test.ts | 6 +-- 10 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 mirage/views/subjects-acceptable.ts diff --git a/app/models/node.ts b/app/models/node.ts index 50e8d464c50..1e845311d3c 100644 --- a/app/models/node.ts +++ b/app/models/node.ts @@ -120,6 +120,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/lib/osf-components/addon/components/node-metadata-form/template.hbs b/lib/osf-components/addon/components/node-metadata-form/template.hbs index b3ddc9be6f1..9c60de6fd3d 100644 --- a/lib/osf-components/addon/components/node-metadata-form/template.hbs +++ b/lib/osf-components/addon/components/node-metadata-form/template.hbs @@ -331,25 +331,27 @@ {{/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/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..7938e99ea2f 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,7 +54,7 @@ export interface SubjectManager { export default class SubjectManagerComponent extends Component { // required model!: ModelWithSubjects; - provider!: ProviderModel; + provider?: ProviderModel; doesAutosave!: boolean; // optional @@ -156,8 +159,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 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/mirage/config.ts b/mirage/config.ts index 9187d16672f..32187f3d1e7 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -28,6 +28,7 @@ import { addCollectionModerator, addRegistrationModerator } from './views/modera import { createNode, storageStatus } from './views/node'; import { osfNestedResource, osfResource, osfToManyRelationship } from './views/osf-resource'; import { getProviderSubjects } from './views/provider-subjects'; +import { getSubjectsAcceptable } from './views/subjects-acceptable'; import { createRegistration, forkRegistration, @@ -119,6 +120,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, 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/views/provider-subjects.ts b/mirage/views/provider-subjects.ts index 3a6e79f1f9a..1951f1b3c39 100644 --- a/mirage/views/provider-subjects.ts +++ b/mirage/views/provider-subjects.ts @@ -2,7 +2,7 @@ import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage import Subject from 'ember-osf-web/models/subject'; import { process } from './utils'; -function getFilterOpts( +export function getFilterOpts( queryParams: { [key: string]: string }, ): { type: string, value: string } { if ('filter[parent]' in queryParams) { 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/tests/acceptance/guid-node/metadata-test.ts b/tests/acceptance/guid-node/metadata-test.ts index f20f41fffcb..a28acb332c6 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]'); From 8b15dbb68814af9c25490aab235e46a76bcc56af Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:25:19 -0400 Subject: [PATCH 02/35] Fix Search on Subject Widget (#2233) ## Purpose Fix Search on Subject Widget ## Summary of Changes Updated lib/osf-components/addon/components/subjects/search/component.ts to handle searching for subjects based on whether the model is a project or not --- .../components/subjects/search/component.ts | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) 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; + } } } From 3a2d8980da9f21ef37940c3415a50c879bd73168 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:04:48 -0400 Subject: [PATCH 03/35] fix URL query param issue (#2237) - Ticket: https://openscience.atlassian.net/browse/ENG-5228 - Feature flag: n/a ## Purpose Fix search text not populating as query parameter and correct filter-value preview counts. ## Summary of Changes - Renamed cardSearchText to q and @query to @cardSearchText in the search page controller and templates. - Adjusted filter-value preview counts to reflect the current search context's result count. --- app/search/controller.ts | 2 +- lib/registries/addon/branded/discover/template.hbs | 2 +- mirage/views/search.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/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: { From 961a036af07fb2dd521be5f8ebbb6b0d66718725 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:59:00 -0400 Subject: [PATCH 04/35] Feature/preprints phase 2 (#2261) * Fix TS variable naming * Implement is-selected style check/listener * Add data-test selector * Clean up css and hbs for mobile * Disable and grey out the create button when no provider is selected * Improve naming for CSS classes * Fix extra spaces in translations/en-us.yml * CR reponse: improve code quality + use +
+ + {{#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..95898a9739c --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/component.ts @@ -0,0 +1,201 @@ +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; + @tracked isPublicDataStatementDisabled = 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; + } + } + + @action + public updateCoi(): void { + if (this.authorAssertionFormChangeset.get('hasCoi')) { + this.authorAssertionFormChangeset.set('conflictOfInterestStatement', null); + this.isConflictOfInterestStatementDisabled = false; + } 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); + } +} 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..72b751e762c --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/component.ts @@ -0,0 +1,60 @@ +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[]) => {}; + 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.ts b/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts new file mode 100644 index 00000000000..ad58154ef92 --- /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 { validateFormat} from 'ember-changeset-validations/validators'; +import buildChangeset from 'ember-osf-web/utils/build-changeset'; +import { tracked } from '@glimmer/tracking'; + + +/** + * The Data Link Args + */ +interface LinkArgs { + remove: (__:number) => {}; + update: (_: string, __:number) => {}; + 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: validateFormat({ + allowBlank: false, + type: 'url', + 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..41d1869d720 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs @@ -0,0 +1,37 @@ +
+ {{#if this.linkFormChangeset}} + +
+ + +
+
+ +
+
+ {{/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..619042b9dba --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs @@ -0,0 +1,24 @@ +
+ {{#each this.links as |link index|}} +
+ +
+ {{/each}} + + +
\ No newline at end of file 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..93b797b8cba --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-data/component.ts @@ -0,0 +1,85 @@ +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; + 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; + } else if (this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.NO) { + this.args.changeSet.set('dataLinks', []); + this.args.changeSet.set('whyNoData', null); + this.isPublicDataWhyNoStatementDisabled = false; + 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..5ed8f34ecf2 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-data/template.hbs @@ -0,0 +1,54 @@ +
+ + {{#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..f887121c0f6 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts @@ -0,0 +1,124 @@ +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; + 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; + } else if (this.args.changeSet.get('hasPreregLinks') === PreprintPreregLinksEnum.NO) { + this.args.changeSet.set('preregLinks', []); + this.args.changeSet.set('whyNoPrereg', null); + this.isPublicPreregistrationWhyNoStatementDisabled = false; + 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..be471ffb7a6 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/template.hbs @@ -0,0 +1,76 @@ +
+ + {{#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..9a7799624b0 --- /dev/null +++ b/app/preprints/-components/submit/author-assertions/template.hbs @@ -0,0 +1,69 @@ +
+

+ {{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..94dd2d88187 --- /dev/null +++ b/app/preprints/-components/submit/file/component.ts @@ -0,0 +1,113 @@ +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; + } + } + + @action + public async validate(file: FileModel): Promise { + 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.isEdit = true; + 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 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..ac9b6d4cd57 --- /dev/null +++ b/app/preprints/-components/submit/file/template.hbs @@ -0,0 +1,112 @@ +
+

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

+ {{#if this.loadFiles.isRunning}} + + {{else}} + {{#if this.isFileAttached}} +
+
+ +
+
+ {{else}} +
+ +
+ + +
+ {{#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..116eae60bc0 --- /dev/null +++ b/app/preprints/-components/submit/file/upload-file/component.ts @@ -0,0 +1,100 @@ +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) { + 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..0a779eebff5 --- /dev/null +++ b/app/preprints/-components/submit/metadata/component.ts @@ -0,0 +1,170 @@ + +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 = true; + @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); + } +} 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..98f3cd7335c --- /dev/null +++ b/app/preprints/-components/submit/metadata/template.hbs @@ -0,0 +1,161 @@ +
+

+ {{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..1a562404190 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss @@ -0,0 +1,55 @@ +// 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; + + .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..8201e157ba5 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs @@ -0,0 +1,128 @@ +
+ {{#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.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..5307f4039c6 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -0,0 +1,616 @@ +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 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'; + +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; + + 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.preprint.currentUserPermissions.includes(Permission.Admin) && + (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 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.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.metadataValidation; + return; + } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && + this.fileValidation + ) { + await this.saveOnStep(); + this.isNextButtonDisabled = !this.authorAssertionValidation; + return; + } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && + this.metadataValidation + ) { + await this.saveOnStep(); + 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'); + const theFiles = await this.preprint.files; + const rootFolder = await theFiles.firstObject!.rootFolder; + const primaryFile = await rootFolder!.files; + this.preprint.set('primaryFile', primaryFile.firstObject); + } +} 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..60fe628658c --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss @@ -0,0 +1,72 @@ +// 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; + + &.cursor { + cursor: pointer; + } + + &.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; + } + + .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..e6ea2e99ebe --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs @@ -0,0 +1,43 @@ +{{#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..60ffb3451ab --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/template.hbs @@ -0,0 +1,41 @@ +{{yield (hash + onDelete=this.onDelete + onWithdrawal=this.onWithdrawal + onClickStep=this.onClickStep + addProjectFile=this.addProjectFile + onNext=this.onNext + onPrevious=this.onPrevious + onSubmit=this.onSubmit + 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 +)}} \ 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..f59384965f1 --- /dev/null +++ b/app/preprints/-components/submit/review/component.ts @@ -0,0 +1,69 @@ +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'; + +/** + * 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; + + constructor(owner: unknown, args: ReviewArgs) { + super(owner, args); + + taskFor(this.loadPreprint).perform(); + } + + @task + @waitFor + private async loadPreprint() { + this.provider = this.preprint.provider.content; + 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..8761d944e74 --- /dev/null +++ b/app/preprints/-components/submit/review/template.hbs @@ -0,0 +1,140 @@ +
+ {{#if this.loadPreprint.isRunning}} + + {{else}} +
+

+ {{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..6b8d5f867b8 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}} @@ -192,6 +192,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..9773c596930 --- /dev/null +++ b/app/preprints/edit/route.ts @@ -0,0 +1,95 @@ +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'; + +@requireAuth() +export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, {}) { + @service store!: Store; + @service theme!: Theme; + @service router!: RouterService; + @service intl!: Intl; + @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; + + const preprint = await this.store.findRecord('preprint', args.guid); + + if ( + !preprint.currentUserPermissions.includes(Permission.Write) || + preprint.isWithdrawn + ) { + throw new Error('User does not have permission to edit this preprint'); + } + + + 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/select/route.ts b/app/preprints/select/route.ts new file mode 100644 index 00000000000..6942843fddc --- /dev/null +++ b/app/preprints/select/route.ts @@ -0,0 +1,26 @@ +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.findAll('preprint-provider', { + reload: true, + adapterOptions: { 'filter[allowSubmissions]': '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 3af5e49cc9f..099d5179d8f 100644 --- a/app/router.ts +++ b/app/router.ts @@ -31,6 +31,9 @@ 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'); }); 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 fac0785e012..adcc9ab19ff 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); 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..02d542fe42b 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 8b7af271d2b..33ffb9ae8f3 100644 --- a/lib/osf-components/addon/components/delete-button/component.ts +++ b/lib/osf-components/addon/components/delete-button/component.ts @@ -29,6 +29,7 @@ export default class DeleteButton extends Component { // Optional arguments small = 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 868fbadf1ee..dbf45900ae8 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='small' {{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}} 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..46de8b7788e 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 @@ -20,8 +20,9 @@ {{/if}}
  • {{t 'navbar.add_a_preprint' preprintWord=(t 'documentType.preprint.singularCapitalized')}} diff --git a/lib/osf-components/addon/components/subjects/manager/component.ts b/lib/osf-components/addon/components/subjects/manager/component.ts index 7938e99ea2f..7f1ec9e08ec 100644 --- a/lib/osf-components/addon/components/subjects/manager/component.ts +++ b/lib/osf-components/addon/components/subjects/manager/component.ts @@ -59,12 +59,15 @@ export default class SubjectManagerComponent extends Component { // optional metadataChangeset?: BufferedChangeset; + onchange?: () => void; + hasSubjects?: (_: boolean) => void; // private @service intl!: Intl; @service toast!: Toast; @service store!: Store; + savedSubjectIds = new Set(); selectedSubjectIds = new Set(); @@ -118,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 @@ -139,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 }); @@ -197,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/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..f08a4c45d68 100644 --- a/lib/osf-components/addon/components/validated-input/text/template.hbs +++ b/lib/osf-components/addon/components/validated-input/text/template.hbs @@ -19,6 +19,7 @@ local-class='PrefixedInput' @type={{if this.password 'password' 'text'}} @value={{this.value}} + maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} />
  • @@ -32,6 +33,7 @@ class='form-control' @type={{if this.password 'password' 'text'}} @value={{this.value}} + maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} /> {{/if}} 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/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 32187f3d1e7..ee5a5acbb0c 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -9,7 +9,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 { @@ -27,7 +27,7 @@ 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, @@ -50,6 +50,7 @@ import { import { updatePassword } from './views/user-password'; import * as userSettings from './views/user-setting'; import * as wb from './views/wb'; +import { createPreprint } from './views/preprint'; const { OSF: { apiUrl, shareBaseUrl, url: osfUrl } } = config; @@ -324,11 +325,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/', @@ -340,11 +350,16 @@ export default function(this: Server) { */ osfResource(this, 'preprint'); + this.post('/preprints', createPreprint); + 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', @@ -355,16 +370,19 @@ export default function(this: Server) { defaultSortKey: 'index', relatedModelName: 'file', }); + + 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..b81583a7136 100644 --- a/mirage/factories/preprint.ts +++ b/mirage/factories/preprint.ts @@ -19,6 +19,8 @@ function buildLicenseText(): string { export interface PreprintMirageModel extends PreprintModel { isPreprintDoi: boolean; addLicenseName: boolean; + nodeId: number; + licenseId: number; } export interface PreprintTraits { @@ -61,6 +63,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 +145,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, }); diff --git a/mirage/fixtures/preprint-providers.ts b/mirage/fixtures/preprint-providers.ts index e15de80a363..0e4a7d28c63 100644 --- a/mirage/fixtures/preprint-providers.ts +++ b/mirage/fixtures/preprint-providers.ts @@ -22,6 +22,7 @@ const preprintProviders: Array> = [ footerLinks: 'fake footer links', reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION, allowCommenting: true, + allowSubmissions: true, }, { id: 'thesiscommons', @@ -31,6 +32,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 +44,7 @@ const preprintProviders: Array> = [ footerLinks: 'Removed in mirage scenario', reviewsCommentsPrivate: true, reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION, + allowSubmissions: true, }, { id: 'paperxiv', @@ -51,6 +54,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 +64,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 +73,7 @@ const preprintProviders: Array> = [ preprintWord: 'work', assets: randomAssets(6), footerLinks: 'fake footer links', + allowSubmissions: true, }, { id: 'docrxiv', @@ -76,6 +82,7 @@ const preprintProviders: Array> = [ preprintWord: 'default', assets: randomAssets(7), footerLinks: 'fake footer links', + allowSubmissions: true, }, { id: 'agrixiv', @@ -84,6 +91,7 @@ const preprintProviders: Array> = [ preprintWord: 'preprint', assets: randomAssets(8), reviewsWorkflow: PreprintProviderReviewsWorkFlow.POST_MODERATION, + allowSubmissions: true, }, { id: 'biohackrxiv', @@ -91,6 +99,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(9), + allowSubmissions: true, }, { id: 'nutrixiv', @@ -98,6 +107,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10), + allowSubmissions: true, }, { id: 'paleorxiv', @@ -105,6 +115,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10, false), + allowSubmissions: true, }, { id: 'sportrxiv', @@ -112,6 +123,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 c6e933e5702..f8166062d9d 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -14,6 +14,7 @@ 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'; const { mirageScenarios, @@ -43,6 +44,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); diff --git a/mirage/scenarios/preprints.ts b/mirage/scenarios/preprints.ts index 440c060be20..81f44752c18 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,8 +58,15 @@ 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, }); @@ -63,7 +74,7 @@ function buildOSF( 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/preprint.ts b/mirage/serializers/preprint.ts index 46a883b6e18..582c0cf11a5 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, }; @@ -44,16 +44,20 @@ 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 1951f1b3c39..b092f045df5 100644 --- a/mirage/views/provider-subjects.ts +++ b/mirage/views/provider-subjects.ts @@ -1,5 +1,6 @@ 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'; export function getFilterOpts( @@ -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/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/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/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/translations/en-us.yml b/translations/en-us.yml index bd6cbdbcd47..bd9d1023537 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -24,92 +24,96 @@ documentType: pluralCapitalized: Theses singular: thesis singularCapitalized: Thesis -contact: +contact: email: support@osf.io general: - OSF: OSF - share: Share - embed: Embed - download: Download - download_url: 'Download url' - done: Done - delete: Delete - view: View - edit: Edit - cancel: Cancel add: Add - ok: OK - apply: Apply - revisions: Revisions - md5: MD5 - date: Date - sha2: SHA2 - title: Title - contributors: Contributors - modified: Modified - description: Description - create: Create and: and - or: or + api: API + apply: Apply + asc_paren: (asc) + available: 'Available' + back: Back bookmark: Bookmark - more: more - upload: Upload - rename: Rename + cancel: Cancel + caution: Caution + close: Close + component: component + contributors: Contributors copy: Copy - move: Move - name: Name - size: Size - version: Version + cos: 'Center for Open Science' + create: Create + date: Date + delete: Delete + desc_paren: (desc) + description: Description + done: Done + download: Download + download_url: 'Download url' downloads: Downloads - close: Close - back: Back - public: Public - filter: Filter - revert: Revert - save: Save + edit: Edit ellipsis: … - warning: Warning - caution: Caution - sort_asc: 'Sort ascending' - sort_desc: 'Sort descending' + embed: Embed + engineering: 'engineering' + filter: Filter + help: Help + home: Home + hosted_on_the_osf: 'Hosted on OSF' last_modified: 'Last modified' - sort: Sort - asc_paren: (asc) - desc_paren: (desc) loading: Loading... + md5: MD5 + modified: Modified + more: more + move: Move + name: Name 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' @@ -579,64 +583,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: @@ -742,6 +746,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 @@ -1160,35 +1170,163 @@ 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: + 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: + title: 'File' + upload-title: 'Upload your {singularPreprintWord}' + uploaded-file-title: 'Attached {singularPreprintWord} file' + file-upload-label: 'Upload from your computer' + file-upload-label-one: 'Drag and drop files here to upload' + file-upload-label-two: 'or click to browse for files.' + file-select-label: 'Select from an existing OSF project' + 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}.' + delete-modal-button: 'Continue' + delete-modal-button-tooltip: 'Version file' + 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' + 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: + title: 'Review' + preprint-service: '{singularPreprintWord} Service' + preprint-title: 'Title' + contributors: 'Contributors' + publication-date: 'Publication Date' + publication-doi: 'Publication DOI' + publication-citation: 'Publication Citation' + conflict-of-interest: 'Conflict of Interest' + no-conflict-of-interest: 'Author asserted no Conflict of Interest.' + public-data: 'Public Data' + public-preregistration: 'Public Preregistration' + supplement-title: 'OSF Project' + supplement-na: 'Author did not add any supplements for this {singularPreprintWord}' + 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: + 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' @@ -1204,7 +1342,7 @@ preprints: author-assertions: header_label: 'Author Assertions' describe: 'Describe' - available: + available: yes: 'Yes' no: 'No' available: 'Available' @@ -1235,10 +1373,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.' @@ -1249,7 +1387,7 @@ preprints: rejected: 'rejected' pending_withdrawal: 'pending withdrawal' withdrawal_rejected: 'withdrawal rejected' - feedback: + feedback: moderator_feedback: 'Moderator feedback' moderator: 'Moderator' base: 'This {documentType}' @@ -1277,13 +1415,13 @@ 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' registries: @@ -1567,12 +1705,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' @@ -2505,13 +2642,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' From 5f18a04d6dfd43896966e88ebe24774a3a85ce6f Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 8 Jul 2024 13:04:20 -0400 Subject: [PATCH 05/35] Add CHANGELOG. Bump version no. --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd9dddac85..5ca5cf43404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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.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 +1988,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Quick Files +[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/package.json b/package.json index 7f6db4367bf..fcaf948d0ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.04.0", + "version": "24.05.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", From 438fee849bf3a76a0df2165535d626837111e148 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 8 Jul 2024 17:39:38 -0400 Subject: [PATCH 06/35] hotfix to filter providers by allow_submissions --- app/preprints/select/route.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/preprints/select/route.ts b/app/preprints/select/route.ts index 6942843fddc..dc214fea5a9 100644 --- a/app/preprints/select/route.ts +++ b/app/preprints/select/route.ts @@ -11,10 +11,11 @@ export default class PreprintSelectRoute extends Route { @service store!: Store; @service theme!: Theme; - async model(){ - const submissionProviders: PreprintProviderModel[] = await this.store.findAll('preprint-provider', { - reload: true, - adapterOptions: { 'filter[allowSubmissions]': 'true' }, + async model() { + const submissionProviders: PreprintProviderModel[] = await this.store.query('preprint-provider', { + filter: { + allow_submissions: true, + }, }); this.theme.set('id', config.defaultProvider); From 0156bf73f76051cb899870b5cd2c4fa64b2fed42 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Tue, 9 Jul 2024 13:17:15 -0500 Subject: [PATCH 07/35] Fixes for eng-5940 and eng-5942 --- app/models/provider.ts | 2 +- .../preprint-state-machine/component.ts | 6 +++-- .../status-flow-display/styles.scss | 8 ++++--- .../status-flow-display/template.hbs | 5 ++-- .../-components/submit/review/component.ts | 24 ++++++++++++++++++- .../-components/submit/review/template.hbs | 17 +++++++++++++ translations/en-us.yml | 16 ++++++++----- 7 files changed, 63 insertions(+), 15 deletions(-) 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/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts index 5307f4039c6..0efa83c457b 100644 --- a/app/preprints/-components/submit/preprint-state-machine/component.ts +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -607,10 +607,12 @@ export default class PreprintStateMachine extends Component{ @task @waitFor public async addProjectFile(file: FileModel): Promise{ - await file.copy(this.preprint, '/', 'osfstorage'); + 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.firstObject); + this.preprint.set('primaryFile', primaryFile.lastObject); } } 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 index 60fe628658c..b203917bbaa 100644 --- 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 @@ -10,9 +10,6 @@ background-color: inherit; z-index: 1; - &.cursor { - cursor: pointer; - } &.selected { width: 193px; @@ -57,6 +54,11 @@ font-weight: bold; } + &.cursor { + cursor: pointer; + } + + .finished { color: $brand-success; } diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs index e6ea2e99ebe..8ad088e1e29 100644 --- a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs @@ -1,7 +1,6 @@ {{#if this.shouldDisplayStatusType}}
    @@ -20,7 +19,9 @@ /> {{/if}}
    -
    +
    {{#if this.isFinished}}
    {{#if this.isFileUploadDisplayed}} diff --git a/app/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts index 0efa83c457b..7122776c368 100644 --- a/app/preprints/-components/submit/preprint-state-machine/component.ts +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -229,20 +229,24 @@ export default class PreprintStateMachine extends Component{ this.args.setPageDirty(); } this.isNextButtonDisabled = true; - if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.titleAndAbstract) && + + if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.titleAndAbstract) && this.titleAndAbstractValidation ) { await this.saveOnStep(); await this.preprint.files; - this.isNextButtonDisabled = !this.metadataValidation; + this.isNextButtonDisabled = !this.fileValidation; return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && this.fileValidation ) { await this.saveOnStep(); - this.isNextButtonDisabled = !this.authorAssertionValidation; + this.isNextButtonDisabled = !this.metadataValidation; return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && this.metadataValidation ) { await this.saveOnStep(); @@ -252,13 +256,15 @@ export default class PreprintStateMachine extends Component{ this.isNextButtonDisabled = !this.supplementValidation; } return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.authorAssertions) && + } 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) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.supplements) && this.supplementValidation ) { await this.saveOnStep(); diff --git a/app/preprints/-components/submit/review/template.hbs b/app/preprints/-components/submit/review/template.hbs index 5c8f05d2474..b37c9e00c0e 100644 --- a/app/preprints/-components/submit/review/template.hbs +++ b/app/preprints/-components/submit/review/template.hbs @@ -16,6 +16,9 @@
    {{this.providerAgreement}}
    +
    + {{t 'preprints.submit.step-review.agreement-provider-two' htmlSafe=true}} +
    {{/if}}
    diff --git a/translations/en-us.yml b/translations/en-us.yml index 6491eb1bb8e..85ce3962b30 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1187,17 +1187,18 @@ preprints: abstract-input: 'Abstract' abstract-input-error: '20 characters' step-file: - title: 'File' - upload-title: 'Upload your {singularPreprintWord}' + 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}.' uploaded-file-title: 'Attached {singularPreprintWord} file' + file-select-label: 'Select from an existing OSF project' file-upload-label: 'Upload from your computer' file-upload-label-one: 'Drag and drop files here to upload' file-upload-label-two: 'or click to browse for files.' - file-select-label: 'Select from an existing OSF project' - 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}.' - delete-modal-button: 'Continue' - delete-modal-button-tooltip: 'Version file' + project-select-explanation: 'A file from a project can only be selected once.' + title: 'File' + upload-title: 'Upload your {singularPreprintWord}' step-metadata: title: 'Metadata' contributors-input: 'Contributors' @@ -1254,8 +1255,8 @@ preprints: 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. You can read more about - OSF preprints moderation policies on the OSF support center.' + 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' From 7b847ddb8e1f94a4a2305e91be4270551620ee80 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Thu, 11 Jul 2024 14:31:15 -0500 Subject: [PATCH 09/35] Fixed the strings for selecting a file from a project --- .../-components/submit/file/component.ts | 10 +++++- .../-components/submit/file/template.hbs | 35 +++++++++++-------- translations/en-us.yml | 5 +-- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/app/preprints/-components/submit/file/component.ts b/app/preprints/-components/submit/file/component.ts index b6871936a8b..64ee093aaff 100644 --- a/app/preprints/-components/submit/file/component.ts +++ b/app/preprints/-components/submit/file/component.ts @@ -50,6 +50,10 @@ export default class PreprintFile extends Component{ } } + public get isSelectProjectButtonDisplayed(): boolean { + return !this.args.manager.isEditFlow; + } + public get isSelectProjectButtonDisabled(): boolean { return this.isButtonDisabled || this.isEdit; } @@ -111,9 +115,13 @@ export default class PreprintFile extends Component{ 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/template.hbs b/app/preprints/-components/submit/file/template.hbs index c4f97f1034e..ee0e08e2e55 100644 --- a/app/preprints/-components/submit/file/template.hbs +++ b/app/preprints/-components/submit/file/template.hbs @@ -4,6 +4,11 @@ > {{t 'preprints.submit.step-file.title'}} + {{#if this.isSelectProjectButtonDisplayed}} +

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

    + {{/if}} {{#if this.loadFiles.isRunning}} {{else}} @@ -34,20 +39,22 @@ > {{ t 'preprints.submit.step-file.file-upload-label'}} - + {{#if this.isSelectProjectButtonDisplayed}} + + {{/if}}
    {{#if this.isFileUploadDisplayed}} {{#let (unique-id 'preprint-upload-files-dropzone') as |id|}} diff --git a/translations/en-us.yml b/translations/en-us.yml index 85ce3962b30..a6b8deac377 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1191,14 +1191,15 @@ preprints: 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}.' - uploaded-file-title: 'Attached {singularPreprintWord} file' file-select-label: 'Select from an existing OSF project' file-upload-label: 'Upload 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 from a project can only be selected once.' + 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: 'You cannot switch options once a file is attached.' step-metadata: title: 'Metadata' contributors-input: 'Contributors' From 07315973b5b85c6379922d1526ddd351d2faf703 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Tue, 16 Jul 2024 01:49:46 -0400 Subject: [PATCH 10/35] add hover text --- app/preprints/-components/submit/file/template.hbs | 8 +++++++- translations/en-us.yml | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/preprints/-components/submit/file/template.hbs b/app/preprints/-components/submit/file/template.hbs index ee0e08e2e55..4b7a5615e5e 100644 --- a/app/preprints/-components/submit/file/template.hbs +++ b/app/preprints/-components/submit/file/template.hbs @@ -11,7 +11,7 @@ {{/if}} {{#if this.loadFiles.isRunning}} - {{else}} + {{else}} {{#if this.isFileAttached}}
    @@ -38,6 +38,9 @@ disabled={{this.isButtonDisabled}} > {{ t 'preprints.submit.step-file.file-upload-label'}} + + {{t 'preprints.submit.step-file.file-upload-help-text' singularPreprintWord=@manager.provider.documentType.singular}} + {{#if this.isSelectProjectButtonDisplayed}}
    @@ -67,7 +67,7 @@ {{t 'preprints.preprint_card.date_modified'}}
    - {{moment @preprint.dateModified}} + {{moment-format @preprint.dateModified 'YYYY-MM-DD'}}
    @@ -81,14 +81,6 @@ />
    -
    -
    - {{t 'preprints.preprint_card.description'}} -
    -
    - {{@preprint.description}} -
    -
    {{#if (and this.showTags @preprint.tags)}}
    From 3e6c27084cbb8882fec900fab574ccd1960eb57d Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Tue, 27 Aug 2024 13:43:07 -0400 Subject: [PATCH 23/35] formatting changes --- .../-components/preprint-card/component.ts | 6 - .../-components/preprint-card/styles.scss | 122 ++++++------------ .../-components/preprint-card/template.hbs | 4 +- app/preprints/my-preprints/styles.scss | 77 ++++------- app/preprints/my-preprints/template.hbs | 16 ++- public/assets/images/preprints/bg-light.jpg | Bin 0 -> 26861 bytes translations/en-us.yml | 10 +- 7 files changed, 81 insertions(+), 154 deletions(-) create mode 100644 public/assets/images/preprints/bg-light.jpg diff --git a/app/preprints/-components/preprint-card/component.ts b/app/preprints/-components/preprint-card/component.ts index 86e25b57e46..90e7f5f844f 100644 --- a/app/preprints/-components/preprint-card/component.ts +++ b/app/preprints/-components/preprint-card/component.ts @@ -13,7 +13,6 @@ import Toast from 'ember-toastr/services/toast'; import RouterService from '@ember/routing/router-service'; import Intl from 'ember-intl/services/intl'; -import Media from 'ember-responsive'; import template from './template'; import styles from './styles'; @@ -27,7 +26,6 @@ export default class PreprintCard extends Component { @service store!: Store; @service toast!: Toast; @service intl!: Intl; - @service media!: Media; preprint?: Preprint; delete?: (preprint: Preprint) => void; @@ -36,10 +34,6 @@ export default class PreprintCard extends Component { searchUrl = pathJoin(baseURL, 'search'); - get isMobile() { - return this.media.isMobile; - } - 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 index 0df768823d3..43e904ba25e 100644 --- a/app/preprints/-components/preprint-card/styles.scss +++ b/app/preprints/-components/preprint-card/styles.scss @@ -1,29 +1,37 @@ -.preprint-card { - width: 100%; - margin: 10px 0; -} +// stylelint-disable max-nesting-depth, selector-max-compound-selectors -.card-contents { - display: block; - flex-direction: row; -} - -.heading { - display: flex; - flex-direction: column; - flex-wrap: wrap; - justify-content: flex-start; - align-items: flex-start; +.preprint-card { width: 100%; -} - -.ember-content-placeholders-heading__title { - height: 1em; - margin-top: 5px; - margin-bottom: 5px; - - &:first-child { - width: 100%; + margin: 1px 0; + + .card-contents { + display: block; + flex-direction: row; + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; + + .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%; + } + } + } } } @@ -85,32 +93,25 @@ dl { margin-bottom: 10px; -} -dl div { - display: flex; -} + div { + display: flex; -dl dt { - width: 100px; - margin-right: 5px; -} + dt { + width: 110px; // Preserved as originally + margin-right: 5px; + } -dl dd { - flex: 1; + dd { + flex: 1; + } + } } .tags { margin-top: 2px; } -.description { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: calc(100% - 100px); -} - .link { composes: Button from 'osf-components/components/button/styles.scss'; composes: SecondaryButton from 'osf-components/components/button/styles.scss'; @@ -134,45 +135,6 @@ dl dd { padding-left: 0; } -.dropdown { - padding-left: 5px; -} - -.dropdown-button { - padding: 4px 12px; - line-height: 0; -} - -.dropdown-list { - background-color: $color-bg-gray-light; - min-width: 180px; -} - -.dropdown-list ul { - list-style: none; - padding-inline-start: 0; -} - -.dropdown-list li { - margin: 10px 0; -} - -.dropdown-link { - color: $color-link-black; - border-color: transparent; - background-color: $color-bg-gray-light; - min-width: 180px; - text-align: left; - padding: 3px 20px; -} - -.dropdown-link:hover, -.dropdown-link:focus { - cursor: pointer; - background-image: none; - background-color: $color-bg-gray-dark; -} - .list-group-item-heading { margin-top: 0; margin-bottom: 5px; diff --git a/app/preprints/-components/preprint-card/template.hbs b/app/preprints/-components/preprint-card/template.hbs index 5a55fdbf2fc..550b41a2375 100644 --- a/app/preprints/-components/preprint-card/template.hbs +++ b/app/preprints/-components/preprint-card/template.hbs @@ -5,7 +5,7 @@ >

    {{#if @preprint}} @@ -18,7 +18,7 @@ | {{/unless}} - + +

    {{t 'preprints.my_preprints.header'}} @@ -9,10 +9,12 @@

    - {{#if this.preprints.length}} - {{#each this.preprints as |preprint|}} - - {{/each}} - {{/if}} +
    + {{#if this.preprints.length}} + {{#each this.preprints as |preprint|}} + + {{/each}} + {{/if}} +
    diff --git a/public/assets/images/preprints/bg-light.jpg b/public/assets/images/preprints/bg-light.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7fd34b9124a91ae8c6ccc53885051f03dde48ea7 GIT binary patch literal 26861 zcmbV#eNIDvLx);@tk%>NuBg$4{=SJE!$W-tt$gGt%Y~ z^Zn*Alc82JZZ?=U8&$T2L6vv!yg8t$!xK1C@C%5hy$Llz|u`7quFG!&}|Hl z;rEhxv*mWXtJzxFeP~1NpnT8J>F<{8X!-stcLekQ`kBVQuO2Tg+j3`Bb&aELr&IZ? z`Ug$B_w3!*`iGvj&wG9T&>#J$FZ}TXf70{dLx0}?rEuioBVRlE$fIBX#<3H_Cr>^8 z#5ZGOboFO% z{(R|I*MI%aKi~M5cb9Me=70Ql<<`Ib?%)68W7-$wDYk__#m@eZeQn0RjApaRyn*&* zFg{8f-)y$nUDn&1yEhyftgO9fs6=i#{oU`sQo5rt_^)^LeKlXUBfy^<(sN2Hr;mHrn|P>d3*I|w%0j!Y~NAq ztl!(<{A|l)hHJ)iUMz2Cim6&EpV+_-U*ZPT`jif#Lq9m@Xy z=l@pUf?4`km!xu&0mfw7EZr~NdblNAH}bNOMIg zu&8SM&SbN%-3L*1g$*|BiWiNAV;K;jea#$(-WcSyr zc}>8O=a2;gn6!rHa8J7Lx{vXZ^mGW~z-5vN#c4P~5^TAFI~v02%$y|GGwFef@sM7ld@(Apu-r=%F?g^)czLjXiHg<*Q9J2J{ITEoNKbjgk6;8$6 z@~IQPj%KmdcF13OaZcKXU$BQ_RZ6+@oVwuN2nDkRZCWkd+UIv(EGhb4D>jzWBnZ|8y`c&R-LYc=f{Pw z$4STh@dDV^HtVbi2J;e>r&}k3u)m_ zHLjVtUHt?&bQx=$Y3PnD3fL$qX@HgqdmH7=SmxxNu`ET5&6N;_4Wk(__V=wv#R0zq+W&IsF>J z?h;8z*`yKzmcS9QDYhHkW=9j!!9qX2nj-}`fL9z^;<@;Y6F_6UF)`H+*H6z?CoO3h zN{Leq){p`T>or4b7D^b+6QV+5V<+0e6+ybFxL3!5G*<^-B^@JD3P~b3+30)$y=zW1 zjzynIW9eA%6}pE5X8{`I@=#&QAK0yy9F?y@tVgS1KpPXK>Eue%Jhv<2%(_^BV{9q9 ztG45YKxCu-Z2>zPDIh?mGisb}!7*KVc{yN*CX2pdWmqziWZe#HObDy>>R9|%n)k{k zOG@9g`cJnYcHOK`xDDhYx?<@t!U3E=S4X(0+dh8ks7P+sD`e?&5C2rnor*@HInpLz z_-*12Yra7$l7p48-a2e`2tskd)99Ki>8?7RHQpGDIg0@=u50Mj#0e5SM_%A#4Fa(M zQVNg3AcVpmhs)!rN_fQQ;%DY$=~T?-aBUO{3nHKjJcE8Gz##lRPFgyd2GE06;fHx? z*y13~61E*DQJB?J5S&WdG%*}|0dvB3`p3wAooR}%hD|;G5bT-ESpVF=dX?qdSVkBL zhyr6<_v^AAf6JR>n01jx_id>u1lAlP$;opnJckv%F`?XBn8)S)ZrmQm2Bz1X65@bI z#&c0c5sMK}jO`F(!JkU@#^`uLiTz3lKnO3rQw|-Q-wu1rb(zkm=jz2aNYHQ@GAIPE zU?hD581MX>3IqxG(CrA>afuej!W4X=e+2BtkEn(_>9AUItPp%Q7r!FsG{+~IUnOoN ze8!h(^oA=cakWrYF4#XI&=pQw?kAzN`Xfv%hBZN-8u})-+R+k(8I#Le?GOWQ3P_mh z1UYI4ei;^n!FiXEJT6C}b{yCaqW1c^I-tqVH1T)LuLW@fM~qw$2g^Um0I==Oc>_3gSxb1Q z`qXzPY2o5ioiu_0ay*Vp!9sA}SmQt1#C#txux5pH%MJcU-!plN>ku0f3HZ&F|5y;7 zUO;Fj_o*lZDKM=AaaabSHJcCLGUS_6)Fm@4NwSV<;@052(OrTK_tLMJVL{GCwIh^` zMNKo=!gxSrR7^4juyh%k_sF3G>P(|xr-Z{BUwU#8w*F!E*+>yQtkDX<5%EgC>>+61 z7|eSl(45hP-!e$#R<+E&cCL2n^M32SwIi|qaB{<8dFrIYVHo@mIplo(R&v+Ak%c4e z?jNb-mZao^J&JSAFc`_@&Lj_!*p20RAzmZIt;SFBey}HS?v^hRhDI0Pg>aM3{6 zQNQz;U@vM{BpN3P;Df}Z%QzoS*8vsxF_`jqxhwQbdpen?u;{kb!|rPQf3_GW0V7i! znKyXDpCPNIXw|FbNx}Y+dE4=-f?p*^ra^29-04cHet0&+v149?{-Q+{3zv!zh|bC4Zt)I`1od?nv+ zWr+|;5PopN=QiwRaL~*^e*9v+{XNn(LL|cQsj;FzS)(<_U~MI^fi+*X3l8G#bk4cH z<=L%1G=KNEQ&S$p;Ll;%`0;3P%j;=%NqPinJO>-1V1hrY0?)delV)U29esj$#|Mcz zdUo$kT6Pw&0?l8cUF--JY+r8o3ahQJZ#wJk>jnqCj9@-#*)W6zkDVR1oGStU8yan~MvakuFbc%L+zj2y?+>j}W#cFkBfipc4IUUKMsE?` zle{T)bn%_&ZW}W4lSQ?Ov=paVe|Ih;7a~#NhM+J+W2ih6d^AI0%-8PatKv}0oLwEh zK8DPve{5tys?(%OG^Yef9LGcrG!xF`p=W|e(>oIuN`l-Lb>YIx@3Ib(*y_Hqp{pmn zy%D6HBc-!V@n;f#7C6IBjk6Bff=Z+dSLV;DSSUc*u!HExIn}Vbxbl)S_53|$eoi8Z zvb7Dm%OXl+qDNpc$#bDITjRzhSp>6IChtA19`M66fpbR$oXSqVS zDo)?f2YfoRH*1`j-T7xZpx&5r#2Zdp-f9P49RKzC?2RWEZ~OHH=W9TBBfMzzGOPlu zf8`~FlZX3ow$FVVk^8J6(L#}lqAb4I6@ecet-t4KRhrH;9bY_y#0wFevEF?$#dW;{ za{TOhSS7I+%I_gAd^+Z(fCJ*!C=66o-xRNrLcT!#vMyDu1_WWO7w>(A>t@U3)A^* zew@Uj-e;(MFs>V6e~ z>Vt2`&!on9E#5zs%Y%{9^v8JtCUohwGJ$7%#WSJs%{30XY4z#~qT?Oke}dh$bk54j zg{YDu8b{$7$PG1?iKWB(VVT!@K%h>871rI{-DMAFZ;Z^FE=10#4}F~P{ZzoAR<_eG zVdTZapMAOQSKdHF<@0h5T$s_Mh$<^d)5`!!tujmtm_t&PXcYxqVS;3a@t`2SEIc1< z$z9f|ptjW@@<&{hv-j7}9d(%JO+N^=2SefS{oTDPxl=1Jm91x|@l2w}Z?4YFfotS~ zWn34adzi{1?{#K>SFCb_kizJ<@5Jfi*Ti+eLi}dSlduNJ016W6EB(f=lp3S*vr4Yg zm$1UrGRa*cM*%ogeSFsw(z_|)ATcCksFhSz4|iA-CeNg z`?L#dCgnW@64K~?995>#1*skhACLnVCql)hZ;imwrCkB?+&k5i7fNSAeF;e(9}K5r zRhN$sdIOH_wpX8gHRjyBI=}K?Vx{Ty#ealGIdee;Gt4C&6wp!KQG=C5x<}zp6fhDI z-)s|YrPx+L6;qQgRwSWt5UK!h3BV-*1y67R@T?L+u&^iH@FtvlCPkGp1b8wOIZhiu zevO(h(s>E#$*G}|NG>}z$vzeM2$`uJ;2!H`T8R8zQBd-O5-oEM${5M~glGylKZitYj5q^)MVJC%Zdqe2qQ>7hed|aY5G8u)gUjPzNvD&$b{#<~X!&t>U~X5K z^6Am%LG>xBM;X`vL%gL`xXx+l`tG=Mj{?RR+*~YSfZPkz+*m zX7($&8Jhzxm?wRLqHkYAbRd2u<B53IyTCxGHSYf=Zd@SBMJ>1`rE^{CGdn zCsI2ui&MDM!3yM}kIk`fN<(v`Ld6R6alkdxH*@JGr!v;s0YGG*=%d|H(_Rfv;&fb|N;V!-isB3wp46;V*0AENr!7Z(BR zhtJhQ8e8OR^GJuVOi_;!wWbj}R=om964PxD9gYE089FLO=Bf8E zt;a_T3kP~QhXXJNwbCBN74j7G4bbfoT*i&`$d{DBXSFf_l?%u`HXupx4=CxsJ0bE+ zEhbP~k_;7Gy&F{sk_g(EICnHyC@Zc@tS`|SHayz*SNeZTNd2<+-(CHD}*vGXn8 zb2xXwM}(F^6Y*TiQE!W&T93BGGK+--)GfI)Gqf?FzBo(86LkORK5(3*1!d9B_?3F~ z1!W@)R{kvvU6%wFg)+IH!Ax{nqI6oTPph|yxEpGaU%OK>Zw>Scdj1T&>hIk-A$4OEhEpvb!5C3gA7|o8 z7)UjEHV|E=%ii8jC6~UT*G*wfIGH|_GBk3{8X6$KD_Vlhf#N2j`nrfqs9zG6Ybn!R1-qD^3V;yw0hKkUS^@#ByQaC&4@9r?RFlCx)XjVf*|n7S z*!Lt`?ut0)sJ^195tf-` zQPxRD8U3UpQ16BPc?0GG6DsVyWl)smi^edjXSK4v{_fL0Cek4DZ+UyhPPh?Z1F4~J zUvtuuj33H}(-3Sf6gfhLB!IFNeRMDglrqsevF}uC{N`QZw|pM^w=MBE+; zJtn{XSon2Rqhf~x)vD#ENE|*3Qunngy}{Rr9yze)CMp*}h8zmfX3igh5L_>?C%eeU zQ^yD07UcH7<@|}|ay#6nnOjovN??b;5B5C+@rRC<8>7!7P!lo)MT18U*7WWKT9RXE zZ3?Ay1>PI%_H|guNT@ z|K1Vbxcl*LE+C`&wDao7b3M{vcHB3vuRQromjt8bu%WppY}-t?hqlmNEuEFGB3VHJ z>qW}P6FqLBDUtf4*U6g{q^uAdXt;B4CR>QL(liA68+={&<Hn8 zHz+77U%3z(00MM)ZhZU^Fy2E|q{O3W6jCsH0t)%@!8+;-FqnQIWN7@R6H=rvNS zz)CFK{3RPDX8deCu45AYEJsp+-@H_AjQNXKVSr+)(H7a*WgL&S;Fe@ytmMrM@erMG zn|syV8wZglqjtgEKoC?r3gi;j%qblLFq$ZKY;J;TA2uF-K(Hn1M9sxS$Mx~Ou=~8$ zWrR`FBGqRW3}G}DA4huz=>2}I18anTdO$s;IxrnEPvDVjqybtE7Py^cVB7t&lFo{b z$`PW4qXtZ5aES>Gp~k@P&q@2nRWMu*2Q(Gc%i(+=ZMeE-xrK;Sx!e|zLsk7x*MeJvLhkN+Dr%Fb{$XkS zs0qHP9!_p3Yl&>^4IZA8K9!#X#V%i+|LB|9Lc+fcM*bq(aE_VhiQ5bXrUqt9+|?ssO# z$C;KA_!&$^f$HcrfvWJz0+R+=2d!yB5njSF|ecmsay1!C{vhbXx#W*wBW7oe5S-YymsP zag-v zQJ>W9TKic|IrlJ-_e34M;1J1aMmyiEl|^Vo2r3B_Ct3aw+~y*#LPXbJuO2?P}$a*Zknpls5ec{xN!GXKdhZ zus~gnrqzY@g15suX?;@ac*JI+%*t3MaIR*uZ0|e zlDP%{u~3BLG^FzkN?>!EW`yzqfHoAHmx;-u9YCo96|lO&2@at=8!hYcgCnY;2X2g( z04~w15EbH)Lhvr9!IyjiJtSsQZ0S9vGpWSe@l%d<_xSYcqR;>?g=Ep!jM#ult2F?( zH>ajhfW8;XpwbfwLz|?aE8{Do%#bRv1Uu@(XhWrF{)Jqis0924+M#@f6M7*(pqt!{ zQJn;mL1K=CZevfYS78F^`1S&H#3e5CsA2zDtj`pfQDU&{VeATX`)CQyMVCAj zsEa2CNRyN63&cDph%VaBm_M=?8TA7+N^}s62bd2-#4MORT)yA+momdn;X! z)ZnNC0f6z;#SjdgCrufKhIaI-(Kr><6f1;1v7byG<2xLOk+Em zD2B+(0c8hFizW?3Yk-rGwyjEyc zr|T%u_q{}awxOtrYZ?1$lv1vX9?llJOg}(M3ZLRp8#X-9-J7k{+()7@$$wJC2{cdi zvVGSvbImReGp@$qp;<;s$IQWw!Z>Q%Uge0;oG@0@e9Pd(r=lhwoLmnKG+P2v5m2=p z5M#r3*dZ6~ivG-iYYJrQ^pZZVwXtoG8dUDGKP`-5QCj~1`zv4)Pzqw;!U2RJJfT~4 z9mDuG$7r51{O}z3sujNu{8a59{V`GhHI2Y5;Hnkm_8-%(cr+4r8)$gm>iOag@fpx7(O1cmrxQ+)?mY&BbdvZfQu;J>{>J{^E0!b8wbp)k#(SC9eBkHm7 zZV0_&L-2OXZd7wi=P21E`7RNLV~HXkGIM}l-<+U!5K6s#EFB;6W#DaA!Cp|3r@Q?1 z>bxd>nq-cGG4aiD44+r>7s22Udi++VldO)%FJfIf6Oura11XyPZm2}W>9Zz!?C+Ao^c0^r~pEYCW*nO`Y2YSHf zy0*{RA6A@~9zY>?ou*0K4Zrn+9r4AflY_Ywn0p`YP0?WLNkog()Hu*!gmiLPzWjC^ zpeH?4q;Yf&d~6yA!f2iL#E2-j>5Qr3*)?R!j&~fois~s#AMjV8Dwbh2FQjb(^|Kkl ztMt$jro~k(TpfGrB@D<+qpOE`G?X$2oOt+1U?mbKb9%ZD)ATsal<362ts@KPYgr0r z`RX+KwgY-8bb>KcmKLtYLL9YtqEpyE&!=2n?o97z%ohr$kYU55=z_2FBd_4$Y`i7Nb}U|Iu5)J88mS|=x!q&j z8tVu>>5%XhjmeW-vEHhrG0~r#|{QhV=|QSVlB(=kz5^j zA4u0uZXWf>AGb8Z3!lucQJ4X?6#@ml>Mqfgo@nra<^3q(A6yP-MvS2w++h%WfSW$> z{#H`q{i_QfyqlSIeC>?Dw_VOzNzJ$WfLr0&T}x(1)pVKDaOwI;=sW%aVE{ew3pmw~ zRcRorPsO!`^Wtr=$GC=5b&y(@xj%Mf*RkM$I^$O!$-gjE>e;0XVk2jf-sCC*`(o0! zW8aXkg(~84V&%{iFN7R=Ke2xTqP56i#aYf|44n%YLm%WCRJ6VusF+Q1;53U|*3(Cl zG+W8_jfgip0Mn?WlGPMJOgbkA=S}{CA@-O(*#dRXv=*iyYc^^si#3IjPtPbN zbJ$+tzs2JpI}P#RQ!DBFx=fKU^eF774O_}x0;L;UpRr=vZSj<_Yl4)~SdX*njULzB zE^H+;E1#;fRjD_frq#s{BPG+_U##1)ulv9+p3OZdTdt(2U*duz?Ups}CLu>z1oP2D zfZlT?-!`~B^)zQB^rLxc2D4|%AWapjvBw;)@=vIXq+|zDpop9c=G~<)j2oYW>JMA0 zXpW-6Ig?4wo4j*%OFhaH9l^e{ZA$c7H`Z@(wJG6bOWz1iD*3$36cmvsF2O?}<%!V+ zFpH<ki6h z4A9bP+|>G1diHapm=g&XQ4Kn&lqZ3GMqa0FR=x@S)Euo!mb^Rt%V1x0>xp*SOM^0q zlBmCf1mLN!qi2$=BQ+8dfZt%3e>Yx1mSP|7?o{KL@&@E^_mu}Vo6hm%r4Ue%Lg)PoQs$d|@po-ReX(mzV5nlZ^GWdBlnwqBk&tSBo8ZtD9%7#NH&CrzHi z;YI+4wq$@v#gg+rdLU3_QRMDypT-Aak*1LM3TDO zNt=U6z+>d?_D=;vq6bubNQM1-lixp>UEsTi4)h!~_iJEP^hoop1AscP`woY_I%y3S zu8-3?0p5f(RY}a70tc!{Eq|;|UQqt_-hv^A4R=`c?J<|T#N)VEj!W^bFk*(3Nt(!9 z>l;RD9{YpGkF~DxAsWB?+4@?M(7zT2Tto`^M)IY#2gJ-O%tB|TX5j~IPHBgFEwr42 zrrGzZm_v>JrlW<n?3Zv8iwC zNOvb7otEAVeq+dKQ}-g!rbFL{+@A8mIxLZwK&un6IJ>!yJF;$>X0M{f0>{r;m!U7R~)ts%ze2@^K` zWgfg*m_7kQYkM=Cf(1W<7`Y3;Kf4Qiukpw?Md7Cumau)cisG#I#<=`z6y%^9k5qLI z22<*>#`siVsS6WCpq zo-S+_!&Ar%i{J}Rc%cLjrco`oAp@a8cy2Z+`?h6}^L%ra8j>;qLu&Y>jhhpH?hA1A z4^iUVYP?5@e;oN`5z&zye1yRCRH2tE4uDDtJ|zVW*^$t)E77}K&V6kiQK2-D)K7P` z8^`6B--URpP*XL@eL$*N#_!ew#gC?V@YkY=C1J796Ldu+!Egx?t7X3uUBYwD+X>(3 z!zmK_Y4^_KE5yI{SeGLGs7-k%mVgll~9elin7cf%Og?MlYA;WgJuQPjEkWuJOu#G_O{q7D# z*KW87L5-Xvj&=~Swo;D29~>YATE`aYJY!>Ex^Txw?h+-e^C%)&mtDRCJ8I8RM_ega zss?ejdt#fcEc7biPvN82ve%#Ud~MPOsisD))w6H(Vljzh=9d(YM+ygU z)PsNJMChiBm()e#&A_RE1mZ)X)LsI13U0aENNi_#w$28K{jy3KM~#16zSf5&?7|(H z15|)IyKS|xqbkMeK3?IREM#Bd~Q_4dTE zh2?a}pbWP0U?m>R)EIt9O6EOA!AEjnSOWAx%8=vGrNRbR;1{vSWa$lM5X4%yP^9Eq zYZ_aFN_LSkJc86~w+zaL1oApohbwQdYI9D*vGrInTJrf!o$bD0sA@8!K^mT60rTV0 zFrpRGZgAJ5=`3}LLs;7;RUPcm4kV&7G(hgc0bcDo*vj5FdTAbmcmQnji93!G#XOZ=tag2w(r^I)cp%j%ZmUFF#@2Nrh_p4 z0kohDc91nP0N}|~6sFRA39B4o!#$xg7C;`xeMdwuu1Cr?f-1r-qu7!j63u26DmE!inkTc(?)V98VwvRO;C#43OVH@l3gT7e_jMmgO_rygBLrnj1U+^Ic7P+^7%ERqfE+5h_k(w= zAPWXK_mX?aOQLW96`bIIqkN4>Nu?o-39~|xs39KpSP{t3!xRq>h zfZ|bIQlSy;8tq9nVL5ml0Bh4-m;)u*ORG0MeSNBfreK9GU2PVeeMc3d>Nir8PrN&Q z$4ZuhypR%pLd^R{MgBLO!YDefDtwI|CMCTwSYe7G_xMFTIEI%^K*}Ebb-raqtTX;U zyT}G8n?e3ogUpcQ&Kf61riq+u;#<)rS7+*}nU3Hch~%&KcE|8g_@os@0id|0&Qgz> z1xh=VUQrgv{tTA~kEK*{wrct=42<^z*~+09F$+X;MACq1)O+QpUW^LV6G0Y>;KB`= z25J>f#0Wx8_#KKI6t3{8f-<;*ro0Kz`GGnWSveJjl2Edw1Gur}{d|T%!T(oEHhMb^ za`8hM0=~$kDVa2dQKDTNUO?^zNJgm<4~lU(7DE!`ViG+j_Jk<-5EsdwUdB_;BQ+q| z@Ddk+JLDn#$aBAOigUO1=nhNc$^_>mAa9&V^nwobxOE?3LPENoC3#9yp2h(mo;K@4 zL5uax`_jZ@EAccv7ue5FOrZ?gO+)Oc*bp|PgQ0itc9$auDP2?rn?M2>76bYbBf4(q!5!dfuaveA)m-M5wtC1lp4^T!PGGuA9RI^~m zBUIsn5RbYt!=cMI!{g|dbUS49T8L-EDCt1m7jWT#0O^WdCU1InPo7FH1>|}mu3ZqS z1Pgi{J4_Dmk%Itrt;-2fHc%UvEo~BT$lfe8hv#Q8f}MB2+TC6d=x~|vAf~u!+sOyv z!3z%%!*yat5@WCt87aec5e=BZ1D*O%Vx`9ooEQI*&N0SZNP8h&I_+Q?)jgi`h7Ff8 zvl?}w006z0;9CDHx6})J5qwquNt<238#Oecv!TQJ2vNlKC3+=7VJ~v!7Laa;Spj3? z3a8W+2`8=BQP}}ICYnPcKb`_S)Hea$gwRpd#z3SI_1rC^2~r^kNyC!W-WN76N#ZFqO1%D ztB6-pH4AQTcHk7UPeOr|_DYc9P-Nu|jX=fABn!ZS=e7vXBcCdSmWO+(hletvotcTR zvldS^)Fb6`QL!@hOL`C?)}n+jju0wxyja6zRPmV{3ht~UDUMEByZvo85EjUoKce2R zxnC~+e*fq@l$?o5WSoE|30|P#77;~sMqo`7lb~({-x8G&ONwP%QL;kG4ZQ2sY;5$~ z$`1p`S{_+yLSi`ec4h{Vuu<-d|EpeZy{su3BG-YE?oL%4Dp?aUHexDpeM5sl-5;fv#=!D{bH-@3=U(GTS+8KtT%l88bSG~i|3Mmo z9Z_$`UH9MpEvTt?Si29*WS^Rqno#O+e#Q?{dQ$f%J7oZ^5%iuI*w8;3o22+tLUc%s zl#rWP?1WfUKjL)GVW(np9JEH8s}lxABt){OCXR!8Ee)oPlO@*3BAuK`t9q7))LP(O z6(FnJMNp296v^HE@-fZ6V^%>o^ikKY1}rOPMqw-1hg=o#CA^|uf~PEYnaYBH6d&%7 zR>S7U<;$15J5!?%3}AuVh5_`mvIRd7nIGr~`C~{8klk}XuxkdsWYRXk`pK z?|Z?8LS|0~ra$1uB?CB=(O1e)^`r(_O?m3sFH8+}{t8SRMT}oM>ah*Bc5S+(PgKHi zyhL5INr>_De$J}}K7sCZIgOuN485`7&E5_4Ah1BV;J@@uOhl{Sj{TK#DftCJfha-% z+fWyCc_XNHQ5FNm7}Hzaw!DB zay(pdzdS%CzhDUii)N5QF@^vozlL~Mn@xX59VRjG#4|m9PWNi&5Nn&YS54fJ)Q?b$C1Zu@7 zQlJ)Q(m36N&O+jk1#e@eC47(4}+LnGCod6s4E*pwc#MR@BP$Q%o|F9JmKC1=sAr zJJ7&wQ2*p&AX_?Q9Y*t3@P3?+GYR<6pG$P~E+7fRxeXNsy_rBVRnTv4BV}o&T$(n5 zYthCq1|YJF<1jHcdd7G^HdSmmMxFQN;Yk_!dSf7K96-|z>~RuT6X;3n1yK->D~U@G zO^pXn&l!2J{tzl3e>u;BCE@z_pww#e9>asI2w0-q%e$x;S@6z&E+&1aIy1W`=HGA# zB`Y0g5(6fq;_6H@OC^5wvz32DukG%T9q~B$C@;NPGbSTQw;z)*AR1-`a0o!^8=Cse#`a&*AZVO!nwrGm4~? z{R>X9Lj{3Np`vScP{7bXRB1m(I|dW8?Hijs1*XV=3F|FZ(Anj9<9JO8y}*%*Np;X4 zvKB&|-aKE>rk-8?abkexrf0X85h1IJ!#&Njh)y-qVJ?dl6c^|FXOSf52w8odsp$89 z5`PMPfg}Pj87+0042N94!r5l}%7yGSLK7dt&@$ijv#Qv~DRoI`|Q& zNcO2$rZIQtYWKIHt52Cdq?SW=M`nNGD&r1HQNW!iYe#AZG3qw_3i4p?gGvo~3Vv5m zJJ36aB2nWMz2)V1Q4#|2u{&}0H@(Le&L>;Ls3y#CLlQWy&4+KCOI0h%vW)X1P z%RU+W8|}sp~*-9-juWM|rq+;rxB`_MG^(IDw(pBV9JVY@%C*ZASsTAM=bT z?q~&Zv|b#N!@lgt`!S&LAdorNjaS5YG$v6@*W4U-O%7cma=9s#@$${Y zZ2F!a*v>>+q8BNl40EykvhL9+;^5+&v3i6dC=V3EzE+Dw=0ICOwZ}}v6a)}ag54Of zg)$3G{9+5Dg2E3<(Gq}oIEeEcUb`8r(!+SrXT=EU8Vg5N+)1j1stMrfN^A`K%u|| zC1*K>j_UWRn*}#QKh-)BGuURKVtzFVEu7wCT1L4>`Kx-aAtc*Ju^Dj|jDw0$c%1TB zz5`bl@R<@ng}adaCqWV8BDXd6jmtM%xmUyV5tZ{RL8Lsek0KvxS2RqTy%G00D@q!U^Zc+CBXTTd2Hw#V8sEM{$JtmHL}nU&A1 zoHJ3)tz(m)T6=Z*wtRcL+x838TvwKiKMH5ylp01*6d^;QIvuDQM2l3ij_`!0;EC%Z zEuVogSDFgrpF%~(_08sG4+dJO{#FN6OV?{F&DF_P7dYi4sscKZ)o@ITC%fMUF2f>dQVK}{4H;t^ znyh@%vMroRn)f@=P#6a1Lk9K22oa_31L>r7K7t+z72p0S#_2*av8iEu1^I%~FuYN3 zszi^RaZNP(+36N$9iK~ZGedt{DW}1hccVmU;GF!iAA+6kj#&=n;vc-2eieYO1;E>^ zJV?y^UPNS-RLut5^GoIcOj_xn6tg1i7~~LaJ7_4z@szV_z!OCjL;O9RD`Z_o4`4tX z?gO+&qHjQ4$l>6jRClIMv)9@;@icV3<_54II)zWOVR&jQ1UbVKSAsoKLtz$AuE2qa z&Hbx_;E}TabH$)4QMEhN>)n zVHiXN_?m-;%(&ZKg6xsV=_`T#a04{zP?69jKtBmR4bHXV*rlZP*9novk-oGykgnml z@TIJ=_g2+AUK29`N4qJihg;}o~<$TIOE88Z@Qo=#D6+c z(yO9WK*!qwRkSpYV;>^gutI{c_clnumG0JF?DNT2qr&w`p?EdwXX(|mG~WzBV)_@r zuV^B&I3mw?B%#AmP#Ps$2V`Lt@tY*+**a=#zKJ2v(l!uhiGn_{a~@{8(sZITy@R?F zeOi>|3(>{!fulecwP?to1aD-N(-{YPQ$5sz8T4Z2Lq7*oqTKrzT`-n8S&wc(+Ifaiv`@3v&Km;lK zyXn3J)jvmq*Qv_F7KrUpm4d2yZU^amgbFXzlf;`?;Xd^yz7@*bpT^5jS>Q#B%`Tjm zvZqPYT+Ehlc9g+{zXI%W@GKLAzgBA_sj_5McoV6mXwEDHE4?5bvBrq9DO8iAhJn3! zC-?^5)Pb+#nz?X4%uhnVr@U}iBuqY-%y%?{Pb12R5e{VFQLb-z5ff^Gyr9qo6oOA< z{}7az4lUNDMadhKhV4@fniH4OmeQ-u_j(UOC9JMym>=z@yd(|v@RdRHqzIgQEz`om z2n3>_DkZ7qS-w9!y*WT;&-Ek{jR~(ly$^GqoPQHnWQy4Lv zh1$2n{X=tPREAbj3_|4}{W(FAHRaqs7DpPyv^|dafq|~c z%P?5vel&JR9L`>i8IR>$xpqg?i<8#gd?%tA6>e}4u0&B36XP0AkSJmRX@e8xC*2iQ zf%aswGzHaC)R|;Mf)+ouTMk{|XG(c_=nEA8zZ(d0MOCN6JS&rVwgc%jPC}v7zU=EH zsBjX>BvHnbu2ElKKz6A^nr_vz}v8p<_9IU0e6 zuY&jvoY{Q? zV!mgy+eWitjP^qG5dUZ);fG{Z-T*eN6U4a~{)(E~~j4dCW}Bt`%AGrDh6%!4rj@?ahuujyK-eIs#A z3d{f+V6iyC4Expbh-Q~iFv6ri*^ro^>4I-V_qoT{;kA|-0r0djFBmU9Iw-Oe5#>Vj$A|V{&TD?@CCO9J{pQP#*u1}vYt^WLf E17_Xz`Tzg` literal 0 HcmV?d00001 diff --git a/translations/en-us.yml b/translations/en-us.yml index cc1b9411fd9..290f115f40e 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1439,9 +1439,7 @@ preprints: pending: 'Pending' accepted: 'Accepted' rejected: 'Rejected' - timestamp_label: 'Date Created' - last_updated: 'Last Updated' - contributors: 'Contributors' + contributors: 'Contributors:' description: 'Description' private_tooltip: 'This preprint is private' options: 'Options' @@ -1450,9 +1448,9 @@ preprints: update_button: 'Edit' settings: 'Settings' delete: 'Delete' - provider: 'Provider' - date_created: 'Date Created' - date_modified: 'Date Modified' + provider: 'Provider:' + date_created: 'Date Created:' + date_modified: 'Date Modified:' registries: header: osf_registrations: 'OSF Registrations' From 77bd26b04270c4a6e31baf8cd31b96ade0f904a9 Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Fri, 30 Aug 2024 09:21:38 -0400 Subject: [PATCH 24/35] remove unnecessary controller and old logic --- .../-components/preprint-card/template.hbs | 193 ++++++++---------- app/preprints/my-preprints/controller.ts | 7 - app/preprints/my-preprints/template.hbs | 8 +- 3 files changed, 92 insertions(+), 116 deletions(-) delete mode 100644 app/preprints/my-preprints/controller.ts diff --git a/app/preprints/-components/preprint-card/template.hbs b/app/preprints/-components/preprint-card/template.hbs index 550b41a2375..f41cc10294d 100644 --- a/app/preprints/-components/preprint-card/template.hbs +++ b/app/preprints/-components/preprint-card/template.hbs @@ -8,121 +8,106 @@ local-class='card-body {{if (is-mobile) 'mobile'}}' >

    - {{#if @preprint}} - {{#unless @preprint.public}} - - - - {{t 'preprints.preprint_card.private_tooltip'}} - - | - {{/unless}} - - {{@preprint.title}} - - {{else}} - - - - {{/if}} - {{#if @preprint}} -
    - {{#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}} -
    - {{/if}} + {{#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}} +

    - {{#if @preprint}} -
    -
    -
    - {{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.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.contributors'}} +
    + {{t 'preprints.preprint_card.tags'}}
    -
    - {{#if (and this.showTags @preprint.tags)}} -
    -
    - {{t 'preprints.preprint_card.tags'}} -
    -
    - -
    -
    - {{/if}} -
    -
    + {{/if}} +
    +
    + + {{t 'preprints.preprint_card.view_button'}} + + {{#if this.shouldShowUpdateButton}} - {{t 'preprints.preprint_card.view_button'}} + {{t 'preprints.preprint_card.update_button'}} - {{#if this.shouldShowUpdateButton}} - - {{t 'preprints.preprint_card.update_button'}} - - {{/if}} -
    - - {{else}} - - - - {{/if}} + {{/if}} +

    diff --git a/app/preprints/my-preprints/controller.ts b/app/preprints/my-preprints/controller.ts deleted file mode 100644 index 49b38b5cf63..00000000000 --- a/app/preprints/my-preprints/controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -import Controller from '@ember/controller'; - -export default class PreprintsMyPreprintsController extends Controller { - get preprints() { - return this.model; - } -} diff --git a/app/preprints/my-preprints/template.hbs b/app/preprints/my-preprints/template.hbs index a14bdc24e51..7711dd8e0df 100644 --- a/app/preprints/my-preprints/template.hbs +++ b/app/preprints/my-preprints/template.hbs @@ -10,11 +10,9 @@
    - {{#if this.preprints.length}} - {{#each this.preprints as |preprint|}} - - {{/each}} - {{/if}} + {{#each this.model as |preprint|}} + + {{/each}}
    From 1004dc24ac8d8ffe7b52c78b5b10a6c8884d4d1f Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Fri, 30 Aug 2024 16:37:36 -0400 Subject: [PATCH 25/35] Add tests --- .../preprint-card/component-test.ts | 49 +++++++++++++ mirage/config.ts | 5 ++ mirage/serializers/preprint.ts | 72 +++++++++++++------ 3 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 app/preprints/-components/preprint-card/component-test.ts 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/mirage/config.ts b/mirage/config.ts index f563c1e9841..69ba4614ca2 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -354,6 +354,11 @@ 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', diff --git a/mirage/serializers/preprint.ts b/mirage/serializers/preprint.ts index 582c0cf11a5..4856c3bf7ae 100644 --- a/mirage/serializers/preprint.ts +++ b/mirage/serializers/preprint.ts @@ -15,8 +15,10 @@ export default class PreprintSerializer extends ApplicationSerializer) { - const relationships: SerializedRelationships = { - provider: { + const relationships: SerializedRelationships = {}; + + if (model.provider) { + relationships.provider = { data: { id: model.provider.id, type: 'preprint-providers', @@ -27,32 +29,44 @@ export default class PreprintSerializer extends ApplicationSerializer Date: Tue, 3 Sep 2024 11:20:28 -0400 Subject: [PATCH 26/35] remove unused classes and services --- .../-components/preprint-card/component.ts | 11 -------- .../-components/preprint-card/styles.scss | 28 +------------------ 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/app/preprints/-components/preprint-card/component.ts b/app/preprints/-components/preprint-card/component.ts index 90e7f5f844f..eb39f3ceae8 100644 --- a/app/preprints/-components/preprint-card/component.ts +++ b/app/preprints/-components/preprint-card/component.ts @@ -1,18 +1,12 @@ import { tagName } from '@ember-decorators/component'; import Component from '@ember/component'; -import { inject as service } from '@ember/service'; import config from 'ember-osf-web/config/environment'; -import Store from '@ember-data/store'; import { layout } from 'ember-osf-web/decorators/component'; import Preprint from 'ember-osf-web/models/preprint'; -import Analytics from 'ember-osf-web/services/analytics'; import pathJoin from 'ember-osf-web/utils/path-join'; import { Permission } from 'ember-osf-web/models/osf-model'; -import Toast from 'ember-toastr/services/toast'; -import RouterService from '@ember/routing/router-service'; -import Intl from 'ember-intl/services/intl'; import template from './template'; import styles from './styles'; @@ -21,11 +15,6 @@ const { OSF: { url: baseURL } } = config; @layout(template, styles) @tagName('') export default class PreprintCard extends Component { - @service analytics!: Analytics; - @service router!: RouterService; - @service store!: Store; - @service toast!: Toast; - @service intl!: Intl; preprint?: Preprint; delete?: (preprint: Preprint) => void; diff --git a/app/preprints/-components/preprint-card/styles.scss b/app/preprints/-components/preprint-card/styles.scss index 43e904ba25e..fa2cd9be09a 100644 --- a/app/preprints/-components/preprint-card/styles.scss +++ b/app/preprints/-components/preprint-card/styles.scss @@ -82,15 +82,6 @@ } } -.preprint-body { - width: 100%; -} - -.ember-content-placeholders-text__line { - height: 1em; - margin-bottom: 5px; -} - dl { margin-bottom: 10px; @@ -98,7 +89,7 @@ dl { display: flex; dt { - width: 110px; // Preserved as originally + width: 110px; margin-right: 5px; } @@ -122,28 +113,11 @@ dl { } } -.open-badges { - width: 20%; - border-left-width: thin; - border-left-color: $color-bg-gray-light; - border-left-style: solid; - padding-left: 10px; -} - -.open-badges.mobile { - width: 10%; - padding-left: 0; -} - .list-group-item-heading { margin-top: 0; margin-bottom: 5px; } -.pull-right { - float: right !important; -} - .update-button { color: $color-text-blue-dark; } From 752ae182ed709c717363acfb616b87158074a083 Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Tue, 3 Sep 2024 11:09:50 -0400 Subject: [PATCH 27/35] Moved changes to preprints-paginated-list branch --- .../paginated-list/base-data-component.ts | 79 +++++++++++++++++++ .../paginated-list/has-many/component.ts | 48 +++++++++++ .../paginated-list/has-many/template.hbs | 15 ++++ .../paginated-list/layout/component.ts | 47 +++++++++++ .../paginated-list/layout/styles.scss | 11 +++ .../paginated-list/layout/template.hbs | 69 ++++++++++++++++ .../paginated-list/x-header/component.ts | 11 +++ .../paginated-list/x-header/template.hbs | 9 +++ .../paginated-list/x-item/component.ts | 10 +++ .../paginated-list/x-item/styles.scss | 13 +++ .../paginated-list/x-item/template.hbs | 9 +++ .../paginated-list/x-render/component.ts | 9 +++ .../paginated-list/x-render/template.hbs | 1 + .../-components/preprint-card/styles.scss | 2 - app/preprints/my-preprints/styles.scss | 6 ++ app/preprints/my-preprints/template.hbs | 30 ++++++- translations/en-us.yml | 1 + 17 files changed, 364 insertions(+), 6 deletions(-) create mode 100644 app/preprints/-components/paginated-list/base-data-component.ts create mode 100644 app/preprints/-components/paginated-list/has-many/component.ts create mode 100644 app/preprints/-components/paginated-list/has-many/template.hbs create mode 100644 app/preprints/-components/paginated-list/layout/component.ts create mode 100644 app/preprints/-components/paginated-list/layout/styles.scss create mode 100644 app/preprints/-components/paginated-list/layout/template.hbs create mode 100644 app/preprints/-components/paginated-list/x-header/component.ts create mode 100644 app/preprints/-components/paginated-list/x-header/template.hbs create mode 100644 app/preprints/-components/paginated-list/x-item/component.ts create mode 100644 app/preprints/-components/paginated-list/x-item/styles.scss create mode 100644 app/preprints/-components/paginated-list/x-item/template.hbs create mode 100644 app/preprints/-components/paginated-list/x-render/component.ts create mode 100644 app/preprints/-components/paginated-list/x-render/template.hbs diff --git a/app/preprints/-components/paginated-list/base-data-component.ts b/app/preprints/-components/paginated-list/base-data-component.ts new file mode 100644 index 00000000000..439baaa80ac --- /dev/null +++ b/app/preprints/-components/paginated-list/base-data-component.ts @@ -0,0 +1,79 @@ +import Component from '@ember/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import { restartableTask } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; + +import Analytics from 'ember-osf-web/services/analytics'; +import Ready from 'ember-osf-web/services/ready'; + +export interface LoadItemsOptions { + reloading: boolean; +} + +export default abstract class BaseDataComponent extends Component { + // Optional arguments + pageSize = 10; + query?: any; + + // Exposes a reload action the the parent scope. + // Usage: `bindReload=(action (mut this.reload))`, then call `this.reload()` to trigger a reload + // NOTE: Don't use this pattern too often, it could get messy. Try to reserve it for telling + // data-loading components to refresh themselves. + bindReload?: (action: (page?: number) => void) => void; + + // Private properties + @service ready!: Ready; + @service analytics!: Analytics; + + totalCount?: number; + items?: any[]; + errorShown = false; + page = 1; + + async loadItemsTask(_: LoadItemsOptions) { + throw new Error('Must implement loadItemsTask'); + } + + @restartableTask + @waitFor + async loadItemsWrapperTask({ reloading }: LoadItemsOptions) { + const blocker = this.ready.getBlocker(); + + try { + await taskFor(this.loadItemsTask).perform({ reloading }); + blocker.done(); + } catch (e) { + this.set('errorShown', true); + blocker.errored(e); + throw e; + } + } + + didReceiveAttrs() { + this.set('page', 1); + if (this.bindReload) { + this.bindReload(this._doReload.bind(this)); + } + taskFor(this.loadItemsWrapperTask).perform({ reloading: false }); + } + + @action + _doReload(page = 1) { + this.setProperties({ page }); + taskFor(this.loadItemsWrapperTask).perform({ reloading: true }); + } + + @action + next() { + this.incrementProperty('page'); + taskFor(this.loadItemsWrapperTask).perform({ reloading: false }); + } + + @action + previous() { + this.decrementProperty('page'); + taskFor(this.loadItemsWrapperTask).perform({ reloading: false }); + } +} diff --git a/app/preprints/-components/paginated-list/has-many/component.ts b/app/preprints/-components/paginated-list/has-many/component.ts new file mode 100644 index 00000000000..f7166f5695c --- /dev/null +++ b/app/preprints/-components/paginated-list/has-many/component.ts @@ -0,0 +1,48 @@ +import { defineProperty } from '@ember/object'; +import { reads } from '@ember/object/computed'; +import { waitFor } from '@ember/test-waiters'; +import { task } from 'ember-concurrency'; +import { inject as service } from '@ember/service'; +import Store from '@ember-data/store'; +import { layout } from 'ember-osf-web/decorators/component'; +import BaseDataComponent from '../base-data-component'; +import template from './template'; + +@layout(template) +export default class PaginatedHasMany extends BaseDataComponent { + // Services + @service store!: Store; + + // Required arguments + modelName!: string; + + // Optional arguments + usePlaceholders = true; + + // Private properties + @task + @waitFor + async loadItemsTask() { + const items = await this.store.query(this.modelName, { + page: this.page, + 'page[size]': this.pageSize, + ...this.query, + }); + + this.setProperties({ + items: items.toArray(), + totalCount: items.meta.total, + errorShown: false, + }); + } + + init() { + super.init(); + + defineProperty( + this, + 'totalCount', + reads('items.length'), + ); + } +} diff --git a/app/preprints/-components/paginated-list/has-many/template.hbs b/app/preprints/-components/paginated-list/has-many/template.hbs new file mode 100644 index 00000000000..21e10e07392 --- /dev/null +++ b/app/preprints/-components/paginated-list/has-many/template.hbs @@ -0,0 +1,15 @@ +{{#paginated-list/layout + isTable=this.isTable + items=this.items + page=this.page + pageSize=this.pageSize + totalCount=this.totalCount + loading=this.loadItemsWrapperTask.isRunning + errorShown=this.errorShown + next=(action this.next) + previous=(action this.previous) + doReload=(action this._doReload) + as |list| +}} + {{yield list}} +{{/paginated-list/layout}} diff --git a/app/preprints/-components/paginated-list/layout/component.ts b/app/preprints/-components/paginated-list/layout/component.ts new file mode 100644 index 00000000000..ee89fdbb0b2 --- /dev/null +++ b/app/preprints/-components/paginated-list/layout/component.ts @@ -0,0 +1,47 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +import { layout, requiredAction } from 'ember-osf-web/decorators/component'; + +import template from './template'; + +@layout(template) +export default class PaginatedList extends Component { + // Required arguments + items?: unknown[]; + page!: number; + pageSize!: number; + @requiredAction next!: () => void; + @requiredAction previous!: () => void; + @requiredAction doReload!: () => void; + + // Optional arguments + loading = false; + errorShown = false; + totalCount?: number; + + // Private properties + @computed('totalCount', 'pageSize') + get maxPage() { + if (typeof this.totalCount === 'undefined') { + return undefined; + } + return Math.ceil(this.totalCount / this.pageSize); + } + + @computed('maxPage', 'page', 'pageSize', 'totalCount') + get placeholderCount() { + if (typeof this.maxPage === 'undefined' || typeof this.totalCount === 'undefined') { + return this.pageSize / 2; + } + if (this.page < this.maxPage || !(this.totalCount % this.pageSize)) { + return this.pageSize; + } + return this.totalCount % this.pageSize; + } + + @computed('items.length', 'loading', 'placeholderCount') + get paginatorShown(): boolean { + return Boolean((this.items && this.items.length) || (this.loading && this.placeholderCount)); + } +} diff --git a/app/preprints/-components/paginated-list/layout/styles.scss b/app/preprints/-components/paginated-list/layout/styles.scss new file mode 100644 index 00000000000..c5072d71dc2 --- /dev/null +++ b/app/preprints/-components/paginated-list/layout/styles.scss @@ -0,0 +1,11 @@ +.text-center { + text-align: center; +} + +.m-md { + margin: 15px; +} + +.list-group { + padding-left: 0; +} diff --git a/app/preprints/-components/paginated-list/layout/template.hbs b/app/preprints/-components/paginated-list/layout/template.hbs new file mode 100644 index 00000000000..625345df1c4 --- /dev/null +++ b/app/preprints/-components/paginated-list/layout/template.hbs @@ -0,0 +1,69 @@ +{{#if this.errorShown}} +

    {{t 'osf-components.paginated-list.error'}}

    +{{else if (or @items.length (and this.loading this.placeholderCount))}} + {{!-- TODO: Take a look at isTable vs list duplicated code for header and items. --}} + {{#if this.isTable}} + + {{yield (hash + header=(component 'paginated-list/x-header' isTable=this.isTable) + )}} + + {{#if this.loading}} + {{#each (range 0 this.placeholderCount)}} + {{yield (hash + item=(component 'paginated-list/x-item' isTable=this.isTable) + doReload=(action @doReload) + )}} + {{/each}} + {{else if @items.length}} + {{#each @items as |item index|}} + {{#unless item.isDeleted}} + {{yield (hash + item=(component 'paginated-list/x-item' isTable=this.isTable item=item index=index) + doReload=(action @doReload) + )}} + {{/unless}} + {{/each}} + {{/if}} + +
    + {{else}} +
      + {{yield (hash header=(component 'paginated-list/x-header'))}} + {{#if this.loading}} + {{#each (range 0 this.placeholderCount)}} + {{yield (hash + item=(component 'paginated-list/x-item') + doReload=(action @doReload) + )}} + {{/each}} + {{else if @items.length}} + {{#each @items as |item index|}} + {{#unless item.isDeleted}} + {{yield (hash + item=(component 'paginated-list/x-item' item=item index=index) + doReload=(action @doReload) + )}} + {{/unless}} + {{/each}} + {{/if}} +
    + {{/if}} + {{#if this.paginatorShown}} +
    + {{simple-paginator + maxPage=this.maxPage + nextPage=(action @next) + previousPage=(action @previous) + curPage=@page + }} +
    + {{/if}} +{{else if this.loading}} + {{loading-indicator dark=true}} +{{else}} + {{yield (hash + empty=(component 'paginated-list/x-render') + doReload=(action @doReload) + )}} +{{/if}} diff --git a/app/preprints/-components/paginated-list/x-header/component.ts b/app/preprints/-components/paginated-list/x-header/component.ts new file mode 100644 index 00000000000..109bec423c9 --- /dev/null +++ b/app/preprints/-components/paginated-list/x-header/component.ts @@ -0,0 +1,11 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; + +import { layout } from 'ember-osf-web/decorators/component'; + +import template from './template'; + +@layout(template) +@tagName('') // No wrapping div +export default class PaginatedListXHeader extends Component { +} diff --git a/app/preprints/-components/paginated-list/x-header/template.hbs b/app/preprints/-components/paginated-list/x-header/template.hbs new file mode 100644 index 00000000000..8f0eb8b1e50 --- /dev/null +++ b/app/preprints/-components/paginated-list/x-header/template.hbs @@ -0,0 +1,9 @@ +{{#if this.isTable}} + + {{yield}} + +{{else}} +
  • + {{yield}} +
  • +{{/if}} diff --git a/app/preprints/-components/paginated-list/x-item/component.ts b/app/preprints/-components/paginated-list/x-item/component.ts new file mode 100644 index 00000000000..8a76a30342c --- /dev/null +++ b/app/preprints/-components/paginated-list/x-item/component.ts @@ -0,0 +1,10 @@ +import { tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; + +import { layout } from 'ember-osf-web/decorators/component'; +import template from './template'; + +@layout(template) +@tagName('') // No wrapping div +export default class PaginatedRelationXItem extends Component { +} diff --git a/app/preprints/-components/paginated-list/x-item/styles.scss b/app/preprints/-components/paginated-list/x-item/styles.scss new file mode 100644 index 00000000000..57c35ff8ec3 --- /dev/null +++ b/app/preprints/-components/paginated-list/x-item/styles.scss @@ -0,0 +1,13 @@ +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: $color-bg-white; + border: 1px solid $color-border-gray; +} diff --git a/app/preprints/-components/paginated-list/x-item/template.hbs b/app/preprints/-components/paginated-list/x-item/template.hbs new file mode 100644 index 00000000000..147557d96f0 --- /dev/null +++ b/app/preprints/-components/paginated-list/x-item/template.hbs @@ -0,0 +1,9 @@ +{{#if this.isTable}} + + {{yield @item @index}} + +{{else}} +
  • + {{yield @item @index}} +
  • +{{/if}} diff --git a/app/preprints/-components/paginated-list/x-render/component.ts b/app/preprints/-components/paginated-list/x-render/component.ts new file mode 100644 index 00000000000..ebbcd09ffa5 --- /dev/null +++ b/app/preprints/-components/paginated-list/x-render/component.ts @@ -0,0 +1,9 @@ +import Component from '@ember/component'; + +import { layout } from 'ember-osf-web/decorators/component'; +import template from './template'; + +@layout(template) +export default class XRender extends Component { + yieldObj?: any; +} diff --git a/app/preprints/-components/paginated-list/x-render/template.hbs b/app/preprints/-components/paginated-list/x-render/template.hbs new file mode 100644 index 00000000000..f2af26baace --- /dev/null +++ b/app/preprints/-components/paginated-list/x-render/template.hbs @@ -0,0 +1 @@ +{{yield this.yieldObj}} diff --git a/app/preprints/-components/preprint-card/styles.scss b/app/preprints/-components/preprint-card/styles.scss index fa2cd9be09a..359632e2008 100644 --- a/app/preprints/-components/preprint-card/styles.scss +++ b/app/preprints/-components/preprint-card/styles.scss @@ -11,8 +11,6 @@ display: block; padding: 10px 15px; margin-bottom: -1px; - background-color: #fff; - border: 1px solid #ddd; .heading { display: flex; diff --git a/app/preprints/my-preprints/styles.scss b/app/preprints/my-preprints/styles.scss index 7734f71eb9e..fae932d93d4 100644 --- a/app/preprints/my-preprints/styles.scss +++ b/app/preprints/my-preprints/styles.scss @@ -61,3 +61,9 @@ 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 index 7711dd8e0df..023ba3c4dc0 100644 --- a/app/preprints/my-preprints/template.hbs +++ b/app/preprints/my-preprints/template.hbs @@ -1,7 +1,7 @@ {{page-title (t 'preprints.my_preprints.header')}} - +

    {{t 'preprints.my_preprints.header'}} @@ -10,9 +10,31 @@
    - {{#each this.model as |preprint|}} - - {{/each}} +
    + {{t 'preprints.my_preprints.sorted'}} +
    + + + + {{#if preprint}} + + {{else}} + {{placeholder.text lines=1}} + {{/if}} + + + +
    +

    {{t 'preprints.noPreprints'}}

    +
    +
    +
    +
    diff --git a/translations/en-us.yml b/translations/en-us.yml index 290f115f40e..8ab788a7686 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1434,6 +1434,7 @@ preprints: 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' From 9f3160b24dc0597cde74042ebb21e810698fb7ce Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Wed, 4 Sep 2024 15:49:19 -0400 Subject: [PATCH 28/35] remove redundant pagination component --- .../paginated-list/base-data-component.ts | 79 ------------------- .../paginated-list/has-many/component.ts | 48 ----------- .../paginated-list/has-many/template.hbs | 15 ---- .../paginated-list/layout/component.ts | 47 ----------- .../paginated-list/layout/styles.scss | 11 --- .../paginated-list/layout/template.hbs | 69 ---------------- .../paginated-list/x-header/component.ts | 11 --- .../paginated-list/x-header/template.hbs | 9 --- .../paginated-list/x-item/component.ts | 10 --- .../paginated-list/x-item/styles.scss | 13 --- .../paginated-list/x-item/template.hbs | 9 --- .../paginated-list/x-render/component.ts | 9 --- .../paginated-list/x-render/template.hbs | 1 - app/preprints/my-preprints/route.ts | 5 +- app/preprints/my-preprints/template.hbs | 8 +- .../paginated-list/has-many/component.ts | 4 - mirage/factories/preprint.ts | 13 +-- 17 files changed, 15 insertions(+), 346 deletions(-) delete mode 100644 app/preprints/-components/paginated-list/base-data-component.ts delete mode 100644 app/preprints/-components/paginated-list/has-many/component.ts delete mode 100644 app/preprints/-components/paginated-list/has-many/template.hbs delete mode 100644 app/preprints/-components/paginated-list/layout/component.ts delete mode 100644 app/preprints/-components/paginated-list/layout/styles.scss delete mode 100644 app/preprints/-components/paginated-list/layout/template.hbs delete mode 100644 app/preprints/-components/paginated-list/x-header/component.ts delete mode 100644 app/preprints/-components/paginated-list/x-header/template.hbs delete mode 100644 app/preprints/-components/paginated-list/x-item/component.ts delete mode 100644 app/preprints/-components/paginated-list/x-item/styles.scss delete mode 100644 app/preprints/-components/paginated-list/x-item/template.hbs delete mode 100644 app/preprints/-components/paginated-list/x-render/component.ts delete mode 100644 app/preprints/-components/paginated-list/x-render/template.hbs diff --git a/app/preprints/-components/paginated-list/base-data-component.ts b/app/preprints/-components/paginated-list/base-data-component.ts deleted file mode 100644 index 439baaa80ac..00000000000 --- a/app/preprints/-components/paginated-list/base-data-component.ts +++ /dev/null @@ -1,79 +0,0 @@ -import Component from '@ember/component'; -import { action } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { waitFor } from '@ember/test-waiters'; -import { restartableTask } from 'ember-concurrency'; -import { taskFor } from 'ember-concurrency-ts'; - -import Analytics from 'ember-osf-web/services/analytics'; -import Ready from 'ember-osf-web/services/ready'; - -export interface LoadItemsOptions { - reloading: boolean; -} - -export default abstract class BaseDataComponent extends Component { - // Optional arguments - pageSize = 10; - query?: any; - - // Exposes a reload action the the parent scope. - // Usage: `bindReload=(action (mut this.reload))`, then call `this.reload()` to trigger a reload - // NOTE: Don't use this pattern too often, it could get messy. Try to reserve it for telling - // data-loading components to refresh themselves. - bindReload?: (action: (page?: number) => void) => void; - - // Private properties - @service ready!: Ready; - @service analytics!: Analytics; - - totalCount?: number; - items?: any[]; - errorShown = false; - page = 1; - - async loadItemsTask(_: LoadItemsOptions) { - throw new Error('Must implement loadItemsTask'); - } - - @restartableTask - @waitFor - async loadItemsWrapperTask({ reloading }: LoadItemsOptions) { - const blocker = this.ready.getBlocker(); - - try { - await taskFor(this.loadItemsTask).perform({ reloading }); - blocker.done(); - } catch (e) { - this.set('errorShown', true); - blocker.errored(e); - throw e; - } - } - - didReceiveAttrs() { - this.set('page', 1); - if (this.bindReload) { - this.bindReload(this._doReload.bind(this)); - } - taskFor(this.loadItemsWrapperTask).perform({ reloading: false }); - } - - @action - _doReload(page = 1) { - this.setProperties({ page }); - taskFor(this.loadItemsWrapperTask).perform({ reloading: true }); - } - - @action - next() { - this.incrementProperty('page'); - taskFor(this.loadItemsWrapperTask).perform({ reloading: false }); - } - - @action - previous() { - this.decrementProperty('page'); - taskFor(this.loadItemsWrapperTask).perform({ reloading: false }); - } -} diff --git a/app/preprints/-components/paginated-list/has-many/component.ts b/app/preprints/-components/paginated-list/has-many/component.ts deleted file mode 100644 index f7166f5695c..00000000000 --- a/app/preprints/-components/paginated-list/has-many/component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { defineProperty } from '@ember/object'; -import { reads } from '@ember/object/computed'; -import { waitFor } from '@ember/test-waiters'; -import { task } from 'ember-concurrency'; -import { inject as service } from '@ember/service'; -import Store from '@ember-data/store'; -import { layout } from 'ember-osf-web/decorators/component'; -import BaseDataComponent from '../base-data-component'; -import template from './template'; - -@layout(template) -export default class PaginatedHasMany extends BaseDataComponent { - // Services - @service store!: Store; - - // Required arguments - modelName!: string; - - // Optional arguments - usePlaceholders = true; - - // Private properties - @task - @waitFor - async loadItemsTask() { - const items = await this.store.query(this.modelName, { - page: this.page, - 'page[size]': this.pageSize, - ...this.query, - }); - - this.setProperties({ - items: items.toArray(), - totalCount: items.meta.total, - errorShown: false, - }); - } - - init() { - super.init(); - - defineProperty( - this, - 'totalCount', - reads('items.length'), - ); - } -} diff --git a/app/preprints/-components/paginated-list/has-many/template.hbs b/app/preprints/-components/paginated-list/has-many/template.hbs deleted file mode 100644 index 21e10e07392..00000000000 --- a/app/preprints/-components/paginated-list/has-many/template.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{{#paginated-list/layout - isTable=this.isTable - items=this.items - page=this.page - pageSize=this.pageSize - totalCount=this.totalCount - loading=this.loadItemsWrapperTask.isRunning - errorShown=this.errorShown - next=(action this.next) - previous=(action this.previous) - doReload=(action this._doReload) - as |list| -}} - {{yield list}} -{{/paginated-list/layout}} diff --git a/app/preprints/-components/paginated-list/layout/component.ts b/app/preprints/-components/paginated-list/layout/component.ts deleted file mode 100644 index ee89fdbb0b2..00000000000 --- a/app/preprints/-components/paginated-list/layout/component.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Component from '@ember/component'; -import { computed } from '@ember/object'; - -import { layout, requiredAction } from 'ember-osf-web/decorators/component'; - -import template from './template'; - -@layout(template) -export default class PaginatedList extends Component { - // Required arguments - items?: unknown[]; - page!: number; - pageSize!: number; - @requiredAction next!: () => void; - @requiredAction previous!: () => void; - @requiredAction doReload!: () => void; - - // Optional arguments - loading = false; - errorShown = false; - totalCount?: number; - - // Private properties - @computed('totalCount', 'pageSize') - get maxPage() { - if (typeof this.totalCount === 'undefined') { - return undefined; - } - return Math.ceil(this.totalCount / this.pageSize); - } - - @computed('maxPage', 'page', 'pageSize', 'totalCount') - get placeholderCount() { - if (typeof this.maxPage === 'undefined' || typeof this.totalCount === 'undefined') { - return this.pageSize / 2; - } - if (this.page < this.maxPage || !(this.totalCount % this.pageSize)) { - return this.pageSize; - } - return this.totalCount % this.pageSize; - } - - @computed('items.length', 'loading', 'placeholderCount') - get paginatorShown(): boolean { - return Boolean((this.items && this.items.length) || (this.loading && this.placeholderCount)); - } -} diff --git a/app/preprints/-components/paginated-list/layout/styles.scss b/app/preprints/-components/paginated-list/layout/styles.scss deleted file mode 100644 index c5072d71dc2..00000000000 --- a/app/preprints/-components/paginated-list/layout/styles.scss +++ /dev/null @@ -1,11 +0,0 @@ -.text-center { - text-align: center; -} - -.m-md { - margin: 15px; -} - -.list-group { - padding-left: 0; -} diff --git a/app/preprints/-components/paginated-list/layout/template.hbs b/app/preprints/-components/paginated-list/layout/template.hbs deleted file mode 100644 index 625345df1c4..00000000000 --- a/app/preprints/-components/paginated-list/layout/template.hbs +++ /dev/null @@ -1,69 +0,0 @@ -{{#if this.errorShown}} -

    {{t 'osf-components.paginated-list.error'}}

    -{{else if (or @items.length (and this.loading this.placeholderCount))}} - {{!-- TODO: Take a look at isTable vs list duplicated code for header and items. --}} - {{#if this.isTable}} - - {{yield (hash - header=(component 'paginated-list/x-header' isTable=this.isTable) - )}} - - {{#if this.loading}} - {{#each (range 0 this.placeholderCount)}} - {{yield (hash - item=(component 'paginated-list/x-item' isTable=this.isTable) - doReload=(action @doReload) - )}} - {{/each}} - {{else if @items.length}} - {{#each @items as |item index|}} - {{#unless item.isDeleted}} - {{yield (hash - item=(component 'paginated-list/x-item' isTable=this.isTable item=item index=index) - doReload=(action @doReload) - )}} - {{/unless}} - {{/each}} - {{/if}} - -
    - {{else}} -
      - {{yield (hash header=(component 'paginated-list/x-header'))}} - {{#if this.loading}} - {{#each (range 0 this.placeholderCount)}} - {{yield (hash - item=(component 'paginated-list/x-item') - doReload=(action @doReload) - )}} - {{/each}} - {{else if @items.length}} - {{#each @items as |item index|}} - {{#unless item.isDeleted}} - {{yield (hash - item=(component 'paginated-list/x-item' item=item index=index) - doReload=(action @doReload) - )}} - {{/unless}} - {{/each}} - {{/if}} -
    - {{/if}} - {{#if this.paginatorShown}} -
    - {{simple-paginator - maxPage=this.maxPage - nextPage=(action @next) - previousPage=(action @previous) - curPage=@page - }} -
    - {{/if}} -{{else if this.loading}} - {{loading-indicator dark=true}} -{{else}} - {{yield (hash - empty=(component 'paginated-list/x-render') - doReload=(action @doReload) - )}} -{{/if}} diff --git a/app/preprints/-components/paginated-list/x-header/component.ts b/app/preprints/-components/paginated-list/x-header/component.ts deleted file mode 100644 index 109bec423c9..00000000000 --- a/app/preprints/-components/paginated-list/x-header/component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { tagName } from '@ember-decorators/component'; -import Component from '@ember/component'; - -import { layout } from 'ember-osf-web/decorators/component'; - -import template from './template'; - -@layout(template) -@tagName('') // No wrapping div -export default class PaginatedListXHeader extends Component { -} diff --git a/app/preprints/-components/paginated-list/x-header/template.hbs b/app/preprints/-components/paginated-list/x-header/template.hbs deleted file mode 100644 index 8f0eb8b1e50..00000000000 --- a/app/preprints/-components/paginated-list/x-header/template.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#if this.isTable}} - - {{yield}} - -{{else}} -
  • - {{yield}} -
  • -{{/if}} diff --git a/app/preprints/-components/paginated-list/x-item/component.ts b/app/preprints/-components/paginated-list/x-item/component.ts deleted file mode 100644 index 8a76a30342c..00000000000 --- a/app/preprints/-components/paginated-list/x-item/component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { tagName } from '@ember-decorators/component'; -import Component from '@ember/component'; - -import { layout } from 'ember-osf-web/decorators/component'; -import template from './template'; - -@layout(template) -@tagName('') // No wrapping div -export default class PaginatedRelationXItem extends Component { -} diff --git a/app/preprints/-components/paginated-list/x-item/styles.scss b/app/preprints/-components/paginated-list/x-item/styles.scss deleted file mode 100644 index 57c35ff8ec3..00000000000 --- a/app/preprints/-components/paginated-list/x-item/styles.scss +++ /dev/null @@ -1,13 +0,0 @@ -.list-group-item:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; -} - -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: $color-bg-white; - border: 1px solid $color-border-gray; -} diff --git a/app/preprints/-components/paginated-list/x-item/template.hbs b/app/preprints/-components/paginated-list/x-item/template.hbs deleted file mode 100644 index 147557d96f0..00000000000 --- a/app/preprints/-components/paginated-list/x-item/template.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#if this.isTable}} - - {{yield @item @index}} - -{{else}} -
  • - {{yield @item @index}} -
  • -{{/if}} diff --git a/app/preprints/-components/paginated-list/x-render/component.ts b/app/preprints/-components/paginated-list/x-render/component.ts deleted file mode 100644 index ebbcd09ffa5..00000000000 --- a/app/preprints/-components/paginated-list/x-render/component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Component from '@ember/component'; - -import { layout } from 'ember-osf-web/decorators/component'; -import template from './template'; - -@layout(template) -export default class XRender extends Component { - yieldObj?: any; -} diff --git a/app/preprints/-components/paginated-list/x-render/template.hbs b/app/preprints/-components/paginated-list/x-render/template.hbs deleted file mode 100644 index f2af26baace..00000000000 --- a/app/preprints/-components/paginated-list/x-render/template.hbs +++ /dev/null @@ -1 +0,0 @@ -{{yield this.yieldObj}} diff --git a/app/preprints/my-preprints/route.ts b/app/preprints/my-preprints/route.ts index c7232043ca3..d0e65030951 100644 --- a/app/preprints/my-preprints/route.ts +++ b/app/preprints/my-preprints/route.ts @@ -2,13 +2,14 @@ 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() { - const preprints = await this.store.findAll('preprint'); - return preprints; + return this.currentUser.user; } } diff --git a/app/preprints/my-preprints/template.hbs b/app/preprints/my-preprints/template.hbs index 023ba3c4dc0..62efb9ac17d 100644 --- a/app/preprints/my-preprints/template.hbs +++ b/app/preprints/my-preprints/template.hbs @@ -14,10 +14,10 @@ {{t 'preprints.my_preprints.sorted'}}

    - @@ -33,7 +33,7 @@

    {{t 'preprints.noPreprints'}}

    - +
    diff --git a/lib/osf-components/addon/components/paginated-list/has-many/component.ts b/lib/osf-components/addon/components/paginated-list/has-many/component.ts index 9df7d12651f..5f21fa9aa0e 100644 --- a/lib/osf-components/addon/components/paginated-list/has-many/component.ts +++ b/lib/osf-components/addon/components/paginated-list/has-many/component.ts @@ -33,10 +33,6 @@ export default class PaginatedHasMany extends BaseDataComponent { const model = await taskFor(this.getModelTask).perform(); if (this.usePlaceholders) { await taskFor(this.loadRelatedCountTask).perform(reloading); - // Don't bother querying if we already know there's nothing there. - if (this.totalCount === 0) { - return; - } } const items = await model.queryHasMany( this.relationshipName, diff --git a/mirage/factories/preprint.ts b/mirage/factories/preprint.ts index b81583a7136..c8b43052c3e 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 { @@ -170,11 +170,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 }); }, }), From 7360de3506535d536ee5f4170e3fd79d61260367 Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Wed, 4 Sep 2024 16:27:35 -0400 Subject: [PATCH 29/35] add usePlaceholders property --- app/preprints/my-preprints/template.hbs | 1 + .../addon/components/paginated-list/has-many/component.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/preprints/my-preprints/template.hbs b/app/preprints/my-preprints/template.hbs index 62efb9ac17d..31a14ed7971 100644 --- a/app/preprints/my-preprints/template.hbs +++ b/app/preprints/my-preprints/template.hbs @@ -18,6 +18,7 @@ @model={{this.model}} @relationshipName='preprints' @pageSize={{10}} + @usePlaceholders={{false}} as |list| > diff --git a/lib/osf-components/addon/components/paginated-list/has-many/component.ts b/lib/osf-components/addon/components/paginated-list/has-many/component.ts index 5f21fa9aa0e..9df7d12651f 100644 --- a/lib/osf-components/addon/components/paginated-list/has-many/component.ts +++ b/lib/osf-components/addon/components/paginated-list/has-many/component.ts @@ -33,6 +33,10 @@ export default class PaginatedHasMany extends BaseDataComponent { const model = await taskFor(this.getModelTask).perform(); if (this.usePlaceholders) { await taskFor(this.loadRelatedCountTask).perform(reloading); + // Don't bother querying if we already know there's nothing there. + if (this.totalCount === 0) { + return; + } } const items = await model.queryHasMany( this.relationshipName, From f20febeec98feb8ea63f09639e8772a2f7cb43d5 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Fri, 13 Sep 2024 09:52:37 -0500 Subject: [PATCH 30/35] Fix submit button during preprints edit flow Closes: #2326 Ticket: ENG-6226 --- .github/workflows/CI.yml | 10 ++++----- .../preprint-state-machine/component.ts | 21 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d8744ece95b..28ede2217e8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -42,7 +42,7 @@ jobs: key: cached_node_modules_${{ secrets.CACHE_VERSION }}_${{ hashFiles('**/yarn.lock') }} restore-keys: cached_node_modules_${{ secrets.CACHE_VERSION }}_ - run: yarn build:test - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: dist path: ./dist @@ -67,7 +67,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: volta-cli/action@v1 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: dist path: ./dist @@ -91,7 +91,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: volta-cli/action@v1 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: dist path: ./dist @@ -115,7 +115,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: volta-cli/action@v1 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: dist path: ./dist @@ -139,7 +139,7 @@ jobs: steps: - uses: actions/checkout@v2 - uses: volta-cli/action@v1 - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v4 with: name: dist path: ./dist diff --git a/app/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts index b882edd517a..52eb3c9fc2a 100644 --- a/app/preprints/-components/submit/preprint-state-machine/component.ts +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -201,15 +201,18 @@ export default class PreprintStateMachine extends Component{ @waitFor public async onSubmit(): Promise { this.args.resetPageDirty(); - 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(); + 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(); From a14d7012796acdaaf95622ed22c863798afec35f Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 13 Sep 2024 16:30:39 -0400 Subject: [PATCH 31/35] Bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cd5a0691bc..dc0bba1167c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.06.0", + "version": "24.06.1", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", From f7bcde19dc96cc728f94c6df9f463a2e0ac418ae Mon Sep 17 00:00:00 2001 From: Lord Business <113387478+bp-cos@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:55:05 -0500 Subject: [PATCH 32/35] [ENG-5028] [ENG-5919] Preprints Affiliation Project PR (FE) (#2321) * Added a new relationship for the affiliated institutions * updates to the serializers * Updates to the component and initial tests * Added a trait * Added more tests and updates * Added more tests to the scenario * Updates to persist affiliated institutions * Fixed the tests * Added logic for mobile and to only display if there are affilitiated institutions * added functionality that all preprints are selected on create mode. Added a test * Added a description for the institutional affiliations * Added a label for accessibility * add affiliated institution details to preprints * add affiliated institution links and logs to the preprint detail page. * improve logo size and shape * update css to new techniques * make links open in new tab * restrict image sizes * fix single quotes * add tests for preprint affiliations and clean up css * Added some test fixes * Add code for institutions preprint affiliations widget on reviews section * fix component to add title to css and conditional template * refactor to reviewsPage and fix linting error and tests * make Institutional affiliations label disappear if no institutions * add hover-text and simplify css * Updates to make affiliated institutions read-only and fixed a bug on edit * Fixed a test * Updated the logic on selecting and persisting of affiliated institutions * Fixed the tests * Added the ability to make the assertion page read-only for non-admins * Added the cancel button * add tooltip and make add contributor widget only visible to admins * Added a link test * Fixed a missed translation on a cancel button for mobile * Fixed the initial issue with read/write users and updated the tests * Added another test * Updated logic and tests to allow admin and write users access * Fixed a test with new mirage settings * Added logic to fix a bug on preprint edit flow with affiliated institutions * allow write contribs to add affiliations * improve permission handling * Updates to mirage to handle adding and removing affiliated institutions * reintroduce isAdmin * [ENG-5919] Feature/preprints affiliations merged to development (#2319) * fix preprint resubmission workflow * Update test for review-action model to reflect target relationship change * delete review-action relationship from child classes * ENG-6008: Add My Preprints route and page template * setup mirage route and view * change defaultSortKey attribute * Bump version no. Add CHANGELOG * create preprint card * fix date format * formatting changes * remove unnecessary controller and old logic * Add tests * remove unused classes and services * Moved changes to preprints-paginated-list branch * remove redundant pagination component * add usePlaceholders property * Don't double-add relationships * Removed the cancel button * Fixed some typos --------- Co-authored-by: John Tordoff Co-authored-by: Brian J. Geiger Co-authored-by: Brian J. Geiger Co-authored-by: Longze Chen --- app/models/preprint.ts | 8 + .../styles.scss | 50 +++ .../template.hbs | 26 ++ .../institution-manager/component-test.ts | 308 ++++++++++++++++++ .../institution-manager/component.ts | 117 +++++++ .../institution-manager/template.hbs | 6 + .../institution-select-list/component-test.ts | 139 ++++++++ .../institution-select-list/component.ts | 28 ++ .../institution-select-list/styles.scss | 44 +++ .../institution-select-list/template.hbs | 29 ++ .../submit/author-assertions/component.ts | 9 +- .../link-widget/component.ts | 1 + .../link-widget/link/component-test.ts | 177 ++++++++++ .../link-widget/link/component.ts | 1 + .../link-widget/link/template.hbs | 27 +- .../link-widget/template.hbs | 25 +- .../public-data/component.ts | 5 +- .../public-data/template.hbs | 2 + .../public-preregistration/component.ts | 5 +- .../public-preregistration/template.hbs | 3 + .../submit/author-assertions/template.hbs | 3 + .../-components/submit/metadata/component.ts | 6 +- .../-components/submit/metadata/template.hbs | 11 +- .../action-flow/styles.scss | 4 + .../action-flow/template.hbs | 34 +- .../preprint-state-machine/component.ts | 65 +++- .../preprint-state-machine/template.hbs | 8 + .../-components/submit/review/template.hbs | 1 + app/preprints/detail/template.hbs | 2 + app/preprints/edit/route.ts | 10 +- .../contributors/card/readonly/template.hbs | 3 + .../validated-input/text/template.hbs | 2 + mirage/config.ts | 11 + mirage/factories/preprint.ts | 20 +- mirage/scenarios/default.ts | 5 +- .../preprints.affiliated-institutions.ts | 103 ++++++ mirage/scenarios/preprints.ts | 2 +- mirage/serializers/preprint.ts | 42 ++- .../component-test.ts | 65 ++++ translations/en-us.yml | 11 + 40 files changed, 1362 insertions(+), 56 deletions(-) create mode 100644 app/preprints/-components/preprint-affiliated-institutions/styles.scss create mode 100644 app/preprints/-components/preprint-affiliated-institutions/template.hbs create mode 100644 app/preprints/-components/preprint-institutions/institution-manager/component-test.ts create mode 100644 app/preprints/-components/preprint-institutions/institution-manager/component.ts create mode 100644 app/preprints/-components/preprint-institutions/institution-manager/template.hbs create mode 100644 app/preprints/-components/preprint-institutions/institution-select-list/component-test.ts create mode 100644 app/preprints/-components/preprint-institutions/institution-select-list/component.ts create mode 100644 app/preprints/-components/preprint-institutions/institution-select-list/styles.scss create mode 100644 app/preprints/-components/preprint-institutions/institution-select-list/template.hbs create mode 100644 app/preprints/-components/submit/author-assertions/link-widget/link/component-test.ts create mode 100644 mirage/scenarios/preprints.affiliated-institutions.ts create mode 100644 tests/integration/components/preprint-affiliated-institutions/component-test.ts diff --git a/app/models/preprint.ts b/app/models/preprint.ts index 25d301365ef..dd67a495f07 100644 --- a/app/models/preprint.ts +++ b/app/models/preprint.ts @@ -5,6 +5,8 @@ import AbstractNodeModel from 'ember-osf-web/models/abstract-node'; import CitationModel from 'ember-osf-web/models/citation'; import PreprintRequestModel from 'ember-osf-web/models/preprint-request'; import { ReviewsState } from 'ember-osf-web/models/provider'; +import ReviewActionModel from 'ember-osf-web/models/review-action'; +import InstitutionModel from 'ember-osf-web/models/institution'; import ContributorModel from './contributor'; import FileModel from './file'; @@ -81,6 +83,12 @@ export default class PreprintModel extends AbstractNodeModel { @belongsTo('preprint-provider', { inverse: 'preprints' }) provider!: AsyncBelongsTo & PreprintProviderModel; + @hasMany('institution') + affiliatedInstitutions!: AsyncHasMany; + + @hasMany('review-action') + reviewActions!: AsyncHasMany; + @hasMany('contributors', { inverse: 'preprint'}) contributors!: AsyncHasMany & ContributorModel; diff --git a/app/preprints/-components/preprint-affiliated-institutions/styles.scss b/app/preprints/-components/preprint-affiliated-institutions/styles.scss new file mode 100644 index 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-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/submit/author-assertions/component.ts b/app/preprints/-components/submit/author-assertions/component.ts index 95898a9739c..e22655246bb 100644 --- a/app/preprints/-components/submit/author-assertions/component.ts +++ b/app/preprints/-components/submit/author-assertions/component.ts @@ -132,7 +132,6 @@ const AuthorAssertionsFormValidation: ValidationObject = { export default class PublicData extends Component{ @service intl!: Intl; @tracked isConflictOfInterestStatementDisabled = true; - @tracked isPublicDataStatementDisabled = true; authorAssertionFormChangeset = buildChangeset( this.args.manager.preprint, AuthorAssertionsFormValidation, @@ -169,7 +168,7 @@ export default class PublicData extends Component{ this.intl.t('preprints.submit.step-assertions.conflict-of-interest-none')); this.isConflictOfInterestStatementDisabled = true; } else { - this.isConflictOfInterestStatementDisabled = false; + this.isConflictOfInterestStatementDisabled = false || !this.args.manager.isAdmin(); } } @@ -177,7 +176,7 @@ export default class PublicData extends Component{ public updateCoi(): void { if (this.authorAssertionFormChangeset.get('hasCoi')) { this.authorAssertionFormChangeset.set('conflictOfInterestStatement', null); - this.isConflictOfInterestStatementDisabled = false; + this.isConflictOfInterestStatementDisabled = false || !this.args.manager.isAdmin(); } else { this.authorAssertionFormChangeset.set('conflictOfInterestStatement', this.intl.t('preprints.submit.step-assertions.conflict-of-interest-none')); @@ -198,4 +197,8 @@ export default class PublicData extends Component{ this.authorAssertionFormChangeset.execute(); this.args.manager.validateAuthorAssertions(true); } + + public get isElementDisabled(): boolean { + return this.args.manager.isElementDisabled(); + } } diff --git a/app/preprints/-components/submit/author-assertions/link-widget/component.ts b/app/preprints/-components/submit/author-assertions/link-widget/component.ts index 72b751e762c..1a6c3e78e73 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/component.ts +++ b/app/preprints/-components/submit/author-assertions/link-widget/component.ts @@ -10,6 +10,7 @@ import { tracked } from '@glimmer/tracking'; */ interface LinkWidgetArgs { update: (_: string[]) => {}; + disabled: boolean; links: string[]; } diff --git a/app/preprints/-components/submit/author-assertions/link-widget/link/component-test.ts b/app/preprints/-components/submit/author-assertions/link-widget/link/component-test.ts new file mode 100644 index 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 index 962bd531dad..c317cd45b59 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/component.ts @@ -15,6 +15,7 @@ import { tracked } from '@glimmer/tracking'; interface LinkArgs { remove: (__:number) => {}; update: (_: string, __:number) => {}; + disabled: boolean; value: string; placeholder: string; index: number; diff --git a/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs b/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs index 41d1869d720..05a6db923cc 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs +++ b/app/preprints/-components/submit/author-assertions/link-widget/link/template.hbs @@ -10,8 +10,9 @@ >
    - + {{#unless @disabled}} + + {{/unless}}
    {{/if}} diff --git a/app/preprints/-components/submit/author-assertions/link-widget/template.hbs b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs index 619042b9dba..b2bbe9fc33d 100644 --- a/app/preprints/-components/submit/author-assertions/link-widget/template.hbs +++ b/app/preprints/-components/submit/author-assertions/link-widget/template.hbs @@ -7,18 +7,21 @@ @value={{link}} @index={{index}} @placeholder={{@placeholder}} + @disabled={{@disabled}} />
    {{/each}} - -
    \ No newline at end of file + {{#unless @disabled}} + + {{/unless}} +
    diff --git a/app/preprints/-components/submit/author-assertions/public-data/component.ts b/app/preprints/-components/submit/author-assertions/public-data/component.ts index 93b797b8cba..2326082b922 100644 --- a/app/preprints/-components/submit/author-assertions/public-data/component.ts +++ b/app/preprints/-components/submit/author-assertions/public-data/component.ts @@ -16,6 +16,7 @@ interface PublicDataArgs { manager: PreprintStateMachine; changeSet: BufferedChangeset; preprintWord: string; + disabled: boolean; validate: () => {}; } @@ -64,11 +65,11 @@ export default class PublicData extends Component{ public updatePublicDataOptions(): void { if (this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.AVAILABLE) { this.args.changeSet.set('whyNoData', null); - this.isPublicDataWhyNoStatementDisabled = false; + this.isPublicDataWhyNoStatementDisabled = false || !this.args.manager.isAdmin(); } else if (this.args.changeSet.get('hasDataLinks') === PreprintDataLinksEnum.NO) { this.args.changeSet.set('dataLinks', []); this.args.changeSet.set('whyNoData', null); - this.isPublicDataWhyNoStatementDisabled = false; + this.isPublicDataWhyNoStatementDisabled = false || !this.args.manager.isAdmin(); this.placeholder = this.intl.t('preprints.submit.step-assertions.public-data-no-placeholder'); } else { this.args.changeSet.set('dataLinks', []); diff --git a/app/preprints/-components/submit/author-assertions/public-data/template.hbs b/app/preprints/-components/submit/author-assertions/public-data/template.hbs index 5ed8f34ecf2..b46f61e6f9b 100644 --- a/app/preprints/-components/submit/author-assertions/public-data/template.hbs +++ b/app/preprints/-components/submit/author-assertions/public-data/template.hbs @@ -20,6 +20,7 @@ @valuePath={{'hasDataLinks'}} @class='radio-group {{if (is-mobile) 'mobile'}}' @isRequired={{true}} + @disabled={{@disabled}} @options={{this.publicDataOptions}} @onchange={{this.updatePublicDataOptions}} as |radioGroup| @@ -33,6 +34,7 @@
    diff --git a/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts index f887121c0f6..0176ab93ede 100644 --- a/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts +++ b/app/preprints/-components/submit/author-assertions/public-preregistration/component.ts @@ -16,6 +16,7 @@ interface PublicPreregistrationArgs { manager: PreprintStateMachine; changeSet: BufferedChangeset; preprintWord: string; + disabled: boolean; validate: () => {}; } @@ -96,11 +97,11 @@ export default class PublicPreregistration extends Component {{#each this.publicPreregLinkInfoOptions as |infoOption|}} @@ -54,6 +56,7 @@
    diff --git a/app/preprints/-components/submit/author-assertions/template.hbs b/app/preprints/-components/submit/author-assertions/template.hbs index 9a7799624b0..49200a5b681 100644 --- a/app/preprints/-components/submit/author-assertions/template.hbs +++ b/app/preprints/-components/submit/author-assertions/template.hbs @@ -29,6 +29,7 @@ @class='radio-group {{if (is-mobile) 'mobile'}}' @isRequired={{true}} @options={{this.coiOptions}} + @disabled={{this.isElementDisabled}} @onchange={{this.updateCoi}} as |radioGroup| > @@ -52,6 +53,7 @@ @changeSet={{this.authorAssertionFormChangeset}} @preprintWord={{@manager.provider.documentType.singular}} @validate={{this.validate}} + @disabled={{this.isElementDisabled}} @manager={{@manager}} />
    @@ -61,6 +63,7 @@ @changeSet={{this.authorAssertionFormChangeset}} @preprintWord={{@manager.provider.documentType.singular}} @validate={{this.validate}} + @disabled={{this.isElementDisabled}} @manager={{@manager}} />
    diff --git a/app/preprints/-components/submit/metadata/component.ts b/app/preprints/-components/submit/metadata/component.ts index 0a779eebff5..938d324bedf 100644 --- a/app/preprints/-components/submit/metadata/component.ts +++ b/app/preprints/-components/submit/metadata/component.ts @@ -79,7 +79,7 @@ const MetadataFormValidation: ValidationObject = { export default class Metadata extends Component{ @service store!: Store; metadataFormChangeset = buildChangeset(this.args.manager.preprint, MetadataFormValidation); - showAddContributorWidget = true; + showAddContributorWidget = this.args.manager.isAdmin(); @tracked displayRequiredLicenseFields = false; @tracked licenses = [] as LicenseModel[]; license!: LicenseModel; @@ -167,4 +167,8 @@ export default class Metadata extends Component{ this.metadataFormChangeset.execute(); this.args.manager.validateMetadata(true); } + + public get widgetMode(): string { + return this.args.manager.isAdmin() ? 'editable' : 'readonly'; + } } diff --git a/app/preprints/-components/submit/metadata/template.hbs b/app/preprints/-components/submit/metadata/template.hbs index 98f3cd7335c..ab07104f80d 100644 --- a/app/preprints/-components/submit/metadata/template.hbs +++ b/app/preprints/-components/submit/metadata/template.hbs @@ -16,11 +16,20 @@ @preprint={{@manager.preprint}} @shouldShowAdd={{this.showAddContributorWidget}} @toggleAddContributorWidget={{this.toggleAddContributorWidget}} - @widgetMode={{'editable'}} + @widgetMode={{this.widgetMode}} @displayPermissionWarning={{this.displayPermissionWarning}} />
    +
    + + + +
    + +
    {{#if this.isSubmit}} {{#if (is-mobile)}}
    @@ -118,6 +118,38 @@
    {{/if}} {{/if}} + {{!-- {{#if @manager.isEditFlow}} + {{#if (is-mobile)}} +
    + +
    + {{else}} +
    + +
    + {{/if}} + {{/if}} --}} {{#if @manager.isWithdrawalButtonDisplayed}}
    { displayAuthorAssertions = false; @tracked statusFlowIndex = 1; @tracked isEditFlow = false; + affiliatedInstitutions = [] as InstitutionModel[]; constructor(owner: unknown, args: StateMachineArgs) { super(owner, args); @@ -98,7 +100,7 @@ export default class PreprintStateMachine extends Component{ } } - this.isWithdrawalButtonDisplayed = this.preprint.currentUserPermissions.includes(Permission.Admin) && + this.isWithdrawalButtonDisplayed = this.isAdmin() && (this.preprint.reviewsState === ReviewsState.ACCEPTED || this.preprint.reviewsState === ReviewsState.PENDING) && !isWithdrawalRejected; @@ -123,6 +125,16 @@ export default class PreprintStateMachine extends Component{ await this.router.transitionTo('preprints.discover', this.provider.id); } + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onCancel(): Promise { + await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); + } + + /** * Callback for the action-flow component */ @@ -253,6 +265,23 @@ export default class PreprintStateMachine extends Component{ this.metadataValidation ) { await this.saveOnStep(); + + if (this.preprint.currentUserPermissions.includes(Permission.Write)) { + try { + await this.preprint.updateM2MRelationship( + 'affiliatedInstitutions', + this.affiliatedInstitutions, + ); + await this.preprint.reload(); + } catch (e) { + // eslint-disable-next-line max-len + const errorMessage = this.intl.t('preprints.submit.step-metadata.institutions.save-institutions-error'); + captureException(e, { errorMessage }); + this.toast.error(getApiErrorMessage(e), errorMessage); + throw e; + } + } + if (this.displayAuthorAssertions) { this.isNextButtonDisabled = !this.authorAssertionValidation; } else { @@ -624,4 +653,36 @@ export default class PreprintStateMachine extends Component{ const primaryFile = await rootFolder!.files; this.preprint.set('primaryFile', primaryFile.lastObject); } + + @action + public updateAffiliatedInstitution(institution: InstitutionModel): void { + if (this.isInstitutionAffiliated(institution.id)) { + this.affiliatedInstitutions.removeObject(institution); + } else { + this.affiliatedInstitutions.addObject(institution); + } + } + + private isInstitutionAffiliated(id: string): boolean { + return this.affiliatedInstitutions.find( + institution => institution.id === id, + ) !== undefined; + } + + @action + public resetAffiliatedInstitutions(): void { + this.affiliatedInstitutions.length = 0; + } + + public isAdmin(): boolean { + return this.preprint.currentUserPermissions.includes(Permission.Admin); + } + + public isElementDisabled(): boolean { + return !this.isAdmin(); + } + + public isAffiliatedInstitutionsDisabled(): boolean { + return !this.preprint.currentUserPermissions.includes(Permission.Write); + } } diff --git a/app/preprints/-components/submit/preprint-state-machine/template.hbs b/app/preprints/-components/submit/preprint-state-machine/template.hbs index 60ffb3451ab..39b51bbad68 100644 --- a/app/preprints/-components/submit/preprint-state-machine/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/template.hbs @@ -6,6 +6,7 @@ onNext=this.onNext onPrevious=this.onPrevious onSubmit=this.onSubmit + onCancel=this.onCancel preprint=this.preprint provider=this.provider isNextButtonDisabled=this.isNextButtonDisabled @@ -38,4 +39,11 @@ statusFlowIndex=this.statusFlowIndex displayAuthorAssertions=this.displayAuthorAssertions + + updateAffiliatedInstitution=this.updateAffiliatedInstitution + resetAffiliatedInstitutions=this.resetAffiliatedInstitutions + + isAffiliatedInstitutionsDisabled=this.isAffiliatedInstitutionsDisabled + isElementDisabled=this.isElementDisabled + isAdmin=this.isAdmin )}} \ No newline at end of file diff --git a/app/preprints/-components/submit/review/template.hbs b/app/preprints/-components/submit/review/template.hbs index b37c9e00c0e..88af48a6982 100644 --- a/app/preprints/-components/submit/review/template.hbs +++ b/app/preprints/-components/submit/review/template.hbs @@ -76,6 +76,7 @@ />
    +
    diff --git a/app/preprints/detail/template.hbs b/app/preprints/detail/template.hbs index 6b8d5f867b8..a6a14847d5f 100644 --- a/app/preprints/detail/template.hbs +++ b/app/preprints/detail/template.hbs @@ -166,6 +166,8 @@
    + + {{#if this.model.preprint.node.links}}

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

    diff --git a/app/preprints/edit/route.ts b/app/preprints/edit/route.ts index 9773c596930..4156ead0d3a 100644 --- a/app/preprints/edit/route.ts +++ b/app/preprints/edit/route.ts @@ -13,6 +13,7 @@ import PreprintEdit from 'ember-osf-web/preprints/edit/controller'; import Intl from 'ember-intl/services/intl'; import Transition from '@ember/routing/-private/transition'; import { Permission } from 'ember-osf-web/models/osf-model'; +import Toast from 'ember-toastr/services/toast'; @requireAuth() export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, {}) { @@ -21,6 +22,7 @@ export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, { @service router!: RouterService; @service intl!: Intl; @service metaTags!: MetaTags; + @service toast!: Toast; headTags?: HeadTagDef[]; // This does NOT work on chrome and I'm going to leave it just in case @@ -46,10 +48,14 @@ export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, { !preprint.currentUserPermissions.includes(Permission.Write) || preprint.isWithdrawn ) { - throw new Error('User does not have permission to edit this preprint'); + const errorMessage = this.intl.t('preprints.submit.edit-permission-error', + { + singularPreprintWord: provider.documentType.singular, + }); + this.toast.error(errorMessage); + throw new Error(errorMessage); } - return { provider, preprint, diff --git a/lib/osf-components/addon/components/contributors/card/readonly/template.hbs b/lib/osf-components/addon/components/contributors/card/readonly/template.hbs index 2505d9fc28b..74476313327 100644 --- a/lib/osf-components/addon/components/contributors/card/readonly/template.hbs +++ b/lib/osf-components/addon/components/contributors/card/readonly/template.hbs @@ -36,6 +36,9 @@ data-test-contributor-permission={{@contributor.id}} local-class='permission-section' > + + {{t 'osf-components.contributors.permissionsNotEditable' }} + {{t (concat 'osf-components.contributors.permissions.' @contributor.permission)}}
    diff --git a/lib/osf-components/addon/components/validated-input/text/template.hbs b/lib/osf-components/addon/components/validated-input/text/template.hbs index f08a4c45d68..28466252c8d 100644 --- a/lib/osf-components/addon/components/validated-input/text/template.hbs +++ b/lib/osf-components/addon/components/validated-input/text/template.hbs @@ -21,6 +21,7 @@ @value={{this.value}} maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} + ...attributes />
    {{else}} @@ -35,6 +36,7 @@ @value={{this.value}} maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} + ...attributes /> {{/if}} {{/validated-input/x-input-wrapper}} diff --git a/mirage/config.ts b/mirage/config.ts index 69ba4614ca2..a8e595fa164 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -378,6 +378,17 @@ export default function(this: Server) { relatedModelName: 'file', }); + osfNestedResource(this, 'preprint', 'affiliatedInstitutions', { + path: '/preprints/:parentID/institutions/', + defaultSortKey: 'index', + relatedModelName: 'institution', + }); + + osfToManyRelationship(this, 'preprint', 'affiliatedInstitutions', { + only: ['related', 'update', 'add', 'remove'], + path: '/preprints/:parentID/relationships/institutions', + }); + this.put('/preprints/:parentID/files/:fileProviderId/upload', uploadToRoot); // Upload to file provider osfNestedResource(this, 'preprint', 'primaryFile', { diff --git a/mirage/factories/preprint.ts b/mirage/factories/preprint.ts index c8b43052c3e..858e51b5a59 100644 --- a/mirage/factories/preprint.ts +++ b/mirage/factories/preprint.ts @@ -31,6 +31,7 @@ export interface PreprintTraits { acceptedWithdrawalComment: Trait; rejectedWithdrawalNoComment: Trait; reviewAction: Trait; + withAffiliatedInstitutions: Trait; } export default Factory.extend({ @@ -41,7 +42,7 @@ export default Factory.extend({ addLicenseName: true, - currentUserPermissions: [Permission.Admin], + currentUserPermissions: [Permission.Admin, Permission.Write, Permission.Read], reviewsState: ReviewsState.REJECTED, @@ -221,6 +222,23 @@ export default Factory.extend({ }, }), + withAffiliatedInstitutions: trait({ + afterCreate(preprint, server) { + const currentUser = server.schema.users.first(); + const affiliatedInstitutions = server.createList('institution', 3); + const osfInstitution = server.create('institution', { + id: 'osf', + name: 'Main OSF Test Institution', + }); + affiliatedInstitutions.unshift(osfInstitution); + + const institutions = currentUser.institutions; + institutions.models.push(osfInstitution); + currentUser.update({institutions}); + preprint.update({ affiliatedInstitutions }); + }, + }), + reviewAction: trait({ afterCreate(preprint, server) { const creator = server.create('user', { fullName: 'Review action Commentor' }); diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index f8166062d9d..e2872b404e0 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -15,6 +15,7 @@ import { settingsScenario } from './settings'; import { registrationsLiteScenario } from './registrations.lite'; import { registrationsManyProjectsScenario} from './registrations.many-projects'; import { userScenario } from './user'; +import { preprintsAffiliatedInstitutionsScenario } from './preprints.affiliated-institutions'; const { mirageScenarios, @@ -76,7 +77,9 @@ export default function(server: Server) { if (mirageScenarios.includes('preprints')) { preprintsScenario(server, currentUser); } - + if (mirageScenarios.includes('preprints::affiliated-institutions')) { + preprintsAffiliatedInstitutionsScenario(server, currentUser); + } if (mirageScenarios.includes('cedar')) { cedarMetadataRecordsScenario(server); } diff --git a/mirage/scenarios/preprints.affiliated-institutions.ts b/mirage/scenarios/preprints.affiliated-institutions.ts new file mode 100644 index 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 81f44752c18..670bd1d9d04 100644 --- a/mirage/scenarios/preprints.ts +++ b/mirage/scenarios/preprints.ts @@ -68,7 +68,7 @@ function buildOSF( 'http://www.datalink.com/3', ], hasPreregLinks: PreprintPreregLinksEnum.NOT_APPLICABLE, - }); + }, 'withAffiliatedInstitutions'); const osfApprovedAdminIdentifier = server.create('identifier'); diff --git a/mirage/serializers/preprint.ts b/mirage/serializers/preprint.ts index 4856c3bf7ae..7e35ba3b294 100644 --- a/mirage/serializers/preprint.ts +++ b/mirage/serializers/preprint.ts @@ -15,7 +15,24 @@ export default class PreprintSerializer extends ApplicationSerializer) { - const relationships: SerializedRelationships = {}; + const relationships: SerializedRelationships = { + contributors: { + links: { + related: { + href: `${apiUrl}/v2/preprints/${model.id}/contributors`, + meta: this.buildRelatedLinkMeta(model, 'contributors'), + }, + }, + }, + citation: { + links: { + related: { + href: `${apiUrl}/v2/preprints/${model.id}/citation/`, + meta: {}, + }, + }, + }, + }; if (model.provider) { relationships.provider = { @@ -32,12 +49,16 @@ export default class PreprintSerializer extends ApplicationSerializer { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(async function(this: ThisTestContext) { + server.loadFixtures('preprint-providers'); + const osf = server.schema.preprintProviders.find('osf') as ModelInstance; + + const preprintMock = server.create('preprint', { provider: osf }, 'withAffiliatedInstitutions'); + const preprintMockNoInstitutions = server.create('preprint', { provider: osf }); + + const store = this.owner.lookup('service:store'); + const preprint: PreprintModel = await store.findRecord('preprint', preprintMock.id); + const preprintNoInstitutions: PreprintModel = await store.findRecord('preprint', preprintMockNoInstitutions.id); + this.preprintMock = preprint; + this.preprintNoInstitutionsMock = preprintNoInstitutions; + }); + + test('no institutions', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').doesNotExist(); + }); + + test('many institutions', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').exists(); + assert.dom('[data-test-preprint-institution-list]').exists({ count: 4 }); + }); + + test('no institutions reviews', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').doesNotExist(); + }); + + test('many institutions reviews', async function(this: ThisTestContext, assert) { + await render(hbs` + `); + assert.dom('[data-test-preprint-institution-list]').exists(); + assert.dom('[data-test-preprint-institution-list]').exists({ count: 4 }); + }); +}); diff --git a/translations/en-us.yml b/translations/en-us.yml index 8ab788a7686..62dbc33b0f1 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1179,6 +1179,7 @@ preprints: paragraph: 'A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal peer review and published in a scientific journal. Learn More.' create_button: 'Create Preprint' submit: + edit-permission-error: 'User does not have permission to edit this {singularPreprintWord}' title-submit: 'New {documentType}' title-edit: 'Edit {documentType}' step-title: @@ -1215,6 +1216,11 @@ preprints: publication-doi-input: 'Publication DOI' publication-date-input: 'Publication Date' publication-citation-input: 'Publication Citation' + institutions: + label: 'Affiliated Institutions' + save-institutions-error: 'Failed to save affiliated institutions' + load-institutions-error: 'Failed to load affiliated institutions' + description: 'You can affiliate your {singularPreprintWord} with your institution if it is an OSF institutional member and has worked with the Center for Open Science to create a dedicated institutional OSF landing page.' step-assertions: title: 'Author Assertions' conflict-of-interest-input: 'Conflict of Interest' @@ -1284,6 +1290,9 @@ preprints: step-supplements: 'Supplements' step-review: 'Review' action-flow: + cancel: 'Cancel' + cancel-modal-body: 'Are you sure you want to cancel editing? The updates on this page will not be saved.' + cancel-modal-title: 'Cancel Edit' delete: 'Delete' delete-modal-body: 'Are you sure you want to delete the {singularPreprintWord}? This action CAN NOT be undone.' delete-modal-title: 'Delete {singularPreprintWord}' @@ -1341,6 +1350,7 @@ preprints: views: 'Views' metrics_disclaimer: 'Metrics collected since:' supplemental_materials: 'Supplemental Materials' + affiliated_institutions: 'Affiliated Institutions' tags: 'Tags' withdrawn_title: 'Withdrawn: {title}' reason_for_withdrawal: 'Reason for withdrawal' @@ -2691,6 +2701,7 @@ osf-components: button: 'Remove contributor' success: 'You have successfully removed {contributorName}.' errorHeading: 'Could not remove contributor. ' + permissionsNotEditable: 'Only Admins may edit permissions.' reviewActionsList: failedToLoadActions: 'Failed to load moderation history' noActionsFound: 'No moderation history found' From bd5f55449b8b60e3dc40383879de46dbfeee4445 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Wed, 18 Sep 2024 17:15:38 -0400 Subject: [PATCH 33/35] Update changelog and bump version --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96189db37cd..9f51bf7eece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ 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 diff --git a/package.json b/package.json index dc0bba1167c..cebbe4b5bbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.06.1", + "version": "24.07.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", From ccac9ea46dfe1278cb91439eb82c5c5cfd904cbc Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:58:10 -0400 Subject: [PATCH 34/35] [ENG-6011] Link My Preprints on nav bar to new page (#2336) ## Purpose Link 'My Preprints' on navbar to new page ## Summary of Changes Change the link of the My Preprints button on the preprints navbar to link to the new My Preprints page. --------- Co-authored-by: Uditi Mehta --- .../addon/components/osf-navbar/preprint-links/template.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 46de8b7788e..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 @@
  • From 584b7850b9f41340e5bd38caedd7231e8d01d171 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:33:30 -0400 Subject: [PATCH 35/35] Update link for branded preprints (#2338) ## Purpose Update link for branded preprints ## Summary of Changes Change the link of the My Preprints button on the branded preprints navbar Co-authored-by: Uditi Mehta --- lib/app-components/addon/components/branded-navbar/template.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app-components/addon/components/branded-navbar/template.hbs b/lib/app-components/addon/components/branded-navbar/template.hbs index 02d542fe42b..ce625d54020 100644 --- a/lib/app-components/addon/components/branded-navbar/template.hbs +++ b/lib/app-components/addon/components/branded-navbar/template.hbs @@ -82,7 +82,7 @@ {{else if (eq this.theme.providerType 'preprint')}}
  • {{t 'navbar.my_preprints'}}