From 9ff75634dabccd27c7dd6abafd8f67841cbb8702 Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Mon, 26 Aug 2024 09:44:39 -0400 Subject: [PATCH 01/11] create preprint card --- .../-components/preprint-card/component.ts | 47 +++++ .../-components/preprint-card/styles.scss | 187 ++++++++++++++++++ .../-components/preprint-card/template.hbs | 137 +++++++++++++ app/preprints/my-preprints/controller.ts | 7 + app/preprints/my-preprints/route.ts | 14 ++ app/preprints/my-preprints/styles.scss | 92 +++++++++ app/preprints/my-preprints/template.hbs | 21 +- translations/en-us.yml | 21 ++ 8 files changed, 523 insertions(+), 3 deletions(-) create mode 100644 app/preprints/-components/preprint-card/component.ts create mode 100644 app/preprints/-components/preprint-card/styles.scss create mode 100644 app/preprints/-components/preprint-card/template.hbs create mode 100644 app/preprints/my-preprints/controller.ts create mode 100644 app/preprints/my-preprints/route.ts create mode 100644 app/preprints/my-preprints/styles.scss diff --git a/app/preprints/-components/preprint-card/component.ts b/app/preprints/-components/preprint-card/component.ts new file mode 100644 index 00000000000..86e25b57e46 --- /dev/null +++ b/app/preprints/-components/preprint-card/component.ts @@ -0,0 +1,47 @@ +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 Media from 'ember-responsive'; +import template from './template'; +import styles from './styles'; + +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; + @service media!: Media; + + preprint?: Preprint; + delete?: (preprint: Preprint) => void; + showTags = false; + readOnly = false; + + 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 new file mode 100644 index 00000000000..957cffab4fd --- /dev/null +++ b/app/preprints/-components/preprint-card/styles.scss @@ -0,0 +1,187 @@ +.preprint-card { + width: 100%; + margin: 10px 0; +} + +.card-contents { + display: flex; + flex-direction: row; +} + +.heading { + display: flex; + flex-direction: column; + flex-wrap: wrap; + justify-content: flex-start; + align-items: flex-start; + width: 100%; +} + +.ember-content-placeholders-heading__title { + height: 1em; + margin-top: 5px; + margin-bottom: 5px; + + &:first-child { + width: 100%; + } +} + +.label-danger { + background-color: $brand-danger; +} + +.heading > span { + line-height: 1.5; +} + +.label-info { + background-color: darken($brand-info, 15%); +} + +.label-primary { + background-color: #337ab7; +} + +.label { + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: 700; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25em; + display: block; + margin-top: 5px; +} + +.osf-link { + margin-left: 5px; + margin-top: 2px; + font-weight: bold; +} + +.osf-link.mobile { + margin-left: 0; +} + +.body { + width: 80%; + + &.mobile { + width: 90%; + } +} + +.preprint-body { + width: 100%; +} + +.ember-content-placeholders-text__line { + height: 1em; + margin-bottom: 5px; +} + +dl { + margin-bottom: 10px; +} + +dl div { + display: flex; +} + +dl dt { + width: 100px; + margin-right: 5px; +} + +dl 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'; + composes: MediumButton from 'osf-components/components/button/styles.scss'; + + &:hover { + text-decoration: none !important; + } +} + +.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; +} + +.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; +} + +.pull-right { + float: right !important; +} + +.update-button { + color: $color-text-blue-dark; +} diff --git a/app/preprints/-components/preprint-card/template.hbs b/app/preprints/-components/preprint-card/template.hbs new file mode 100644 index 00000000000..366552e41d8 --- /dev/null +++ b/app/preprints/-components/preprint-card/template.hbs @@ -0,0 +1,137 @@ +
+
+
+

+ {{#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}} +

+
+ {{#if @preprint}} +
+
+
+ {{t 'preprints.preprint_card.provider'}} +
+
+ {{@preprint.provider.name}} +
+
+
+
+ {{t 'preprints.preprint_card.date_created'}} +
+
+ {{moment @preprint.dateCreated}} +
+
+
+
+ {{t 'preprints.preprint_card.date_modified'}} +
+
+ {{moment @preprint.dateModified}} +
+
+
+
+ {{t 'preprints.preprint_card.contributors'}} +
+
+ +
+
+
+
+ {{t 'preprints.preprint_card.description'}} +
+
+ {{@preprint.description}} +
+
+ {{#if (and this.showTags @preprint.tags)}} +
+
+ {{t 'preprints.preprint_card.tags'}} +
+
+ +
+
+ {{/if}} +
+
+ + {{t 'preprints.preprint_card.view_button'}} + + {{#if this.shouldShowUpdateButton}} + + {{t 'preprints.preprint_card.update_button'}} + + {{/if}} +
+ + {{else}} + + + + {{/if}} +
+
+
+
diff --git a/app/preprints/my-preprints/controller.ts b/app/preprints/my-preprints/controller.ts new file mode 100644 index 00000000000..49b38b5cf63 --- /dev/null +++ b/app/preprints/my-preprints/controller.ts @@ -0,0 +1,7 @@ +import Controller from '@ember/controller'; + +export default class PreprintsMyPreprintsController extends Controller { + get preprints() { + return this.model; + } +} diff --git a/app/preprints/my-preprints/route.ts b/app/preprints/my-preprints/route.ts new file mode 100644 index 00000000000..c7232043ca3 --- /dev/null +++ b/app/preprints/my-preprints/route.ts @@ -0,0 +1,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'; + +@requireAuth() +export default class PreprintsMyPreprintsRoute extends Route { + @service store!: Store; + + async model() { + const preprints = await this.store.findAll('preprint'); + return preprints; + } +} diff --git a/app/preprints/my-preprints/styles.scss b/app/preprints/my-preprints/styles.scss new file mode 100644 index 00000000000..f5e4bb369bc --- /dev/null +++ b/app/preprints/my-preprints/styles.scss @@ -0,0 +1,92 @@ +// stylelint-disable declaration-property-value-blacklist +// stylelint-disable selector-no-qualifying-type + +.ContentBackground { + display: flex; + flex-grow: 1; + z-index: 1; + justify-content: center; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url('img/bg-light.jpg'); + background-repeat: no-repeat; + background-size: cover; + filter: grayscale(1); + } + + :global(.list-group-item) { + margin-bottom: 20px; + } + + :global(.media-desktop), + :global(.media-tablet), + :global(.media-mobile) { + margin-top: -38px; + } +} + +.Title { + min-height: 150px; +} + +.NavTabs { + ul { + margin-left: 15px; + margin-bottom: 10px; + line-height: 20px; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + padding: 0 0 41px; + box-sizing: border-box; + } + + .NavItem { + border: none; + color: #fff; + background: none; + padding: 10px 15px; + float: left; + + &:hover { + border: none; + color: #333; + background-color: #e4e4e4; + } + } + + :global(.ember-tabs__tab--selected) { + border: none; + color: #333; + background-color: #e4e4e4; + + &:focus { + border: none; + background-color: #e4e4e4; + } + + &:hover { + border: none; + background-color: #e4e4e4; + } + } + + .TabPane { + &:global(.active) { + display: block; + } + } +} + +.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 765bb2a11b4..a955a64093c 100644 --- a/app/preprints/my-preprints/template.hbs +++ b/app/preprints/my-preprints/template.hbs @@ -1,3 +1,18 @@ -
- {{outlet}} -
+{{page-title (t 'preprints.my_preprints.header')}} + + + +
+

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

+
+
+ + {{#if this.preprints.length}} + {{#each this.preprints as |preprint|}} + + {{/each}} + {{/if}} + +
diff --git a/translations/en-us.yml b/translations/en-us.yml index cc9858c2fcb..cc1b9411fd9 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1432,6 +1432,27 @@ preprints: advisory: heading: 'Advisory Group' paragraph: 'Our advisory group includes leaders in preprints and scholarly communication' + my_preprints: + header: 'My Preprints' + preprint_card: + statuses: + pending: 'Pending' + accepted: 'Accepted' + rejected: 'Rejected' + timestamp_label: 'Date Created' + last_updated: 'Last Updated' + contributors: 'Contributors' + description: 'Description' + private_tooltip: 'This preprint is private' + options: 'Options' + manage_contributors: 'Manage Contributors' + view_button: 'View' + update_button: 'Edit' + settings: 'Settings' + delete: 'Delete' + provider: 'Provider' + date_created: 'Date Created' + date_modified: 'Date Modified' registries: header: osf_registrations: 'OSF Registrations' From df7bde523dc389a411e91793912722a66ef57bf4 Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Mon, 26 Aug 2024 12:27:41 -0400 Subject: [PATCH 02/11] fix date format --- app/preprints/-components/preprint-card/styles.scss | 2 +- app/preprints/-components/preprint-card/template.hbs | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/preprints/-components/preprint-card/styles.scss b/app/preprints/-components/preprint-card/styles.scss index 957cffab4fd..0df768823d3 100644 --- a/app/preprints/-components/preprint-card/styles.scss +++ b/app/preprints/-components/preprint-card/styles.scss @@ -4,7 +4,7 @@ } .card-contents { - display: flex; + display: block; flex-direction: row; } diff --git a/app/preprints/-components/preprint-card/template.hbs b/app/preprints/-components/preprint-card/template.hbs index 366552e41d8..5a55fdbf2fc 100644 --- a/app/preprints/-components/preprint-card/template.hbs +++ b/app/preprints/-components/preprint-card/template.hbs @@ -59,7 +59,7 @@ {{t 'preprints.preprint_card.date_created'}}
- {{moment @preprint.dateCreated}} + {{moment-format @preprint.dateCreated 'YYYY-MM-DD'}}
@@ -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 03/11] 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 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 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] 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 09/11] 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 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 10/11] [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 11/11] 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",