From 43f5e9fe1deb0f4db9b51bce5d4e52135510da0d Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Tue, 27 Feb 2024 00:23:42 -0500 Subject: [PATCH 001/193] disable CEDAR embeddable editor and artifact viewer --- app/router.ts | 4 ++-- ember-cli-build.js | 4 ++-- .../components/metadata/metadata-detail/template.hbs | 4 ++-- lib/registries/addon/routes.ts | 2 +- package.json | 2 -- yarn.lock | 10 ---------- 6 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/router.ts b/app/router.ts index 3af5e49cc9f..c1996a16160 100644 --- a/app/router.ts +++ b/app/router.ts @@ -67,7 +67,7 @@ Router.map(function() { this.route('guid-file', { path: '--file/:guid' }, function() { this.route('index', { path: '/'}); this.route('metadata', function() { - this.route('add'); + // this.route('add'); }); }); @@ -80,7 +80,7 @@ Router.map(function() { this.route('metadata', function() { this.route('index', { path: '/'}); this.route('detail', { path: '/:recordId' }); - this.route('add'); + // this.route('add'); }); this.route('registrations'); this.route('drafts', { path: '/drafts/:draftId' }, function() { diff --git a/ember-cli-build.js b/ember-cli-build.js index ebbc8fe6086..6c1da84ccf3 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -113,7 +113,7 @@ module.exports = function(defaults) { app.import('node_modules/dropzone/dist/dropzone.js'); app.import('node_modules/wicg-inert/dist/inert.min.js'); - app.import('node_modules/cedar-embeddable-editor/cedar-embeddable-editor.js'); - app.import('node_modules/cedar-artifact-viewer/cedar-artifact-viewer.js'); + // app.import('node_modules/cedar-embeddable-editor/cedar-embeddable-editor.js'); + // app.import('node_modules/cedar-artifact-viewer/cedar-artifact-viewer.js'); return app.toTree(); }; diff --git a/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs b/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs index 63e73febc39..61ec2f8e61c 100644 --- a/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs +++ b/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs @@ -6,7 +6,7 @@
- {{#if this.hasWritePermission}} + {{!-- {{#if this.hasWritePermission}}
@@ -29,7 +29,7 @@ {{/if}}
- {{/if}} + {{/if}} --}}
diff --git a/lib/registries/addon/routes.ts b/lib/registries/addon/routes.ts index 3b9c9d71c86..da39826bcec 100644 --- a/lib/registries/addon/routes.ts +++ b/lib/registries/addon/routes.ts @@ -47,7 +47,7 @@ export default buildRoutes(function() { this.route('metadata', function() { this.route('index', { path: '/'}); this.route('detail', { path: '/:recordId' }); - this.route('add'); + // this.route('add'); }); }); diff --git a/package.json b/package.json index f3b68ff9f06..0ab14014d18 100644 --- a/package.json +++ b/package.json @@ -117,8 +117,6 @@ "babel-eslint": "^8.0.0", "broccoli-asset-rev": "^3.0.0", "c3": "^0.6.14", - "cedar-artifact-viewer": "^0.9.3", - "cedar-embeddable-editor": "^1.2.1", "chai": "^4.1.2", "coveralls": "^3.0.3", "dropzone": "5.5.1", diff --git a/yarn.lock b/yarn.lock index 4eebcbe6e88..7ffd33553ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9165,16 +9165,6 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -cedar-artifact-viewer@^0.9.3: - version "0.9.5" - resolved "https://registry.yarnpkg.com/cedar-artifact-viewer/-/cedar-artifact-viewer-0.9.5.tgz#0679b8cff2a2be96c0ef9ac8cc567327aba6e2e6" - integrity sha512-o23pXLrLBB6ZgZZW79SaE+c41CEGSASZ9YC0qKd8BK8b2EmLwiH18dEQv5pXYSxKKo3Ue7WdnyLoRNEZ+yo9mQ== - -cedar-embeddable-editor@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cedar-embeddable-editor/-/cedar-embeddable-editor-1.2.1.tgz#e4a27e657b6d63fd4e86506ed4ca77a693b8c51d" - integrity sha512-XyAgvQimvFBCuLuWIvYpb2fEgCKMgEOj9vHP47Heurycs9B6Wbvk3bIvJ8GbqadKA8j83z1772vF3f+65P89qw== - chai-as-promised@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-6.0.0.tgz#1a02a433a6f24dafac63b9c96fa1684db1aa8da6" From ee07dde658cd676fc062f5cb0dc79b40e95171d3 Mon Sep 17 00:00:00 2001 From: Uditi Mehta Date: Mon, 11 Mar 2024 12:35:50 -0400 Subject: [PATCH 002/193] ENG-5275 fixed abstract text copy-paste --- .../addon/components/expandable-preview/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/osf-components/addon/components/expandable-preview/styles.scss b/lib/osf-components/addon/components/expandable-preview/styles.scss index e8ab620090e..2d4b6149d06 100644 --- a/lib/osf-components/addon/components/expandable-preview/styles.scss +++ b/lib/osf-components/addon/components/expandable-preview/styles.scss @@ -9,6 +9,7 @@ right: 0; width: 100%; height: 100%; + pointer-events: none; &.Wrapper__Collapsed { background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); From 2308f63cb77fa2b1b3131dac27259be0879ebc17 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:29:33 -0400 Subject: [PATCH 003/193] ENG-4944 Added "withdrawn" to indexed preprint titles (#2161) * ENG-4944 adding withdrawn to indexed preprint titles * ENG-4944 made requested changes to the head tag and title bar * ENG-4944 Integrate intl for dynamic title localization * Fix title logic in head tags and update yarn.lock --------- Co-authored-by: Uditi Mehta --- app/preprints/detail/controller.ts | 9 +++++++++ app/preprints/detail/route.ts | 9 ++++++++- app/preprints/detail/template.hbs | 4 ++-- translations/en-us.yml | 1 + 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/preprints/detail/controller.ts b/app/preprints/detail/controller.ts index 3b9274c4515..eb31f7f8c47 100644 --- a/app/preprints/detail/controller.ts +++ b/app/preprints/detail/controller.ts @@ -81,6 +81,15 @@ export default class PrePrintsDetailController extends Controller { }); } + get displayTitle(): string { + if (this.model.preprint.isWithdrawn) { + return this.intl.t('preprints.detail.withdrawn_title', { + title: this.model.preprint.title, + }); + } + return this.model.preprint.title; + } + private isAdmin(): boolean { // True if the current user has admin permissions for the node that contains the preprint return (this.model.preprint.currentUserPermissions).includes(Permission.Admin); diff --git a/app/preprints/detail/route.ts b/app/preprints/detail/route.ts index be730199acb..fb1a3e70fe4 100644 --- a/app/preprints/detail/route.ts +++ b/app/preprints/detail/route.ts @@ -17,8 +17,10 @@ import Ready from 'ember-osf-web/services/ready'; import Theme from 'ember-osf-web/services/theme'; import captureException from 'ember-osf-web/utils/capture-exception'; import pathJoin from 'ember-osf-web/utils/path-join'; +import Intl from 'ember-intl/services/intl'; import PrePrintsDetailController from './controller'; + /** * @module ember-preprints * @submodule routes @@ -40,6 +42,7 @@ export default class PreprintsDetail extends Route { @service currentUser!: CurrentUser; @service metaTags!: MetaTags; @service ready!: Ready; + @service intl!: Intl; headTags?: HeadTagDef[]; @@ -116,8 +119,12 @@ export default class PreprintsDetail extends Route { const doi = (identifiers as Identifier[]).find(identifier => identifier.category === 'doi'); const image = 'engines-dist/registries/assets/img/osf-sharing.png'; + const preprintTitle = preprint.isWithdrawn ? + this.intl.t('preprints.detail.withdrawn_title', { title: preprint.title }) : + preprint.title; + const metaTagsData = { - title: preprint.title, + title: preprintTitle, description: preprint.description, publishedDate: moment(preprint.datePublished).format('YYYY-MM-DD'), modifiedDate: moment(preprint.dateModified).format('YYYY-MM-DD'), diff --git a/app/preprints/detail/template.hbs b/app/preprints/detail/template.hbs index c389000ffd5..c0d24703e5b 100644 --- a/app/preprints/detail/template.hbs +++ b/app/preprints/detail/template.hbs @@ -1,4 +1,4 @@ -{{page-title this.model.preprint.title replace=false}} +{{page-title this.displayTitle replace=false}}
-

{{this.model.preprint.title}}

+

{{this.displayTitle}}

{{#unless this.model.preprint.isWithdrawn}}
{{#if (and this.userIsContrib (not this.isPendingWithdrawal))}} diff --git a/translations/en-us.yml b/translations/en-us.yml index 873cae9d67d..2453b52196b 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1196,6 +1196,7 @@ preprints: metrics_disclaimer: 'Metrics collected since:' supplemental_materials: 'Supplemental Materials' tags: 'Tags' + withdrawn_title: 'Withdrawn: {title}' reason_for_withdrawal: 'Reason for withdrawal' file_renderer: download_previous_versions: 'Download previous versions' From 94a69a597b0824d26663ac6e103cced7c7384d5f Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Tue, 19 Mar 2024 11:02:11 -0400 Subject: [PATCH 004/193] ENG-5093 Improve Visibility of Embargoed Registrations on Registration Card (#2166) * ENG-4944 adding withdrawn to indexed preprint titles * ENG-4944 made requested changes to the head tag and title bar * ENG-4944 Integrate intl for dynamic title localization * Fix title logic in head tags and update yarn.lock * ENG-5093 Improve Visibility of Embargoed Registrations on Registration Card * ENG-5093 Improve Visibility of Embargoed Registrations on Registration Card * ENG-5093 remove commented code * ENG-5093 exclude status for withdrawn registrations * ENG-5093 exclude status for rejected registrations * ENG-5093 added getter, fix linitng errors --------- Co-authored-by: Uditi Mehta --- .../registries/registration-list/card/component.ts | 14 ++++++++++++++ .../registries/registration-list/card/template.hbs | 13 ++++++++++++- mirage/scenarios/registrations.full.ts | 8 ++++++-- translations/en-us.yml | 3 +++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/osf-components/addon/components/registries/registration-list/card/component.ts b/lib/osf-components/addon/components/registries/registration-list/card/component.ts index dae5a0c76f0..02a07da29af 100644 --- a/lib/osf-components/addon/components/registries/registration-list/card/component.ts +++ b/lib/osf-components/addon/components/registries/registration-list/card/component.ts @@ -10,6 +10,7 @@ import RegistrationModel, { RegistrationReviewStates } from 'ember-osf-web/model import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; + const iconMap: Partial> = { [RegistrationReviewStates.Pending]: 'hourglass', [RegistrationReviewStates.Withdrawn]: 'ban', @@ -33,6 +34,19 @@ export default class RegistrationListCard extends Component { return this.args.registration.revisionState === RevisionReviewStates.RevisionPendingModeration; } + get showPublic() { + const reviewsState = this.args.registration.reviewsState; + return ![RegistrationReviewStates.Rejected,RegistrationReviewStates.Withdrawn].includes(reviewsState); + } + + get showEmbargo() { + const reviewsState = this.args.registration.reviewsState; + if (this.args.registration.embargoEndDate) { + return ![RegistrationReviewStates.Rejected,RegistrationReviewStates.Withdrawn].includes(reviewsState); + } + return false; + } + get icon(): string { const { state } = this.args; return iconMap[state] || ''; diff --git a/lib/osf-components/addon/components/registries/registration-list/card/template.hbs b/lib/osf-components/addon/components/registries/registration-list/card/template.hbs index 33e3a03197b..c3757724da2 100644 --- a/lib/osf-components/addon/components/registries/registration-list/card/template.hbs +++ b/lib/osf-components/addon/components/registries/registration-list/card/template.hbs @@ -32,6 +32,17 @@ {{/if}} + {{#if this.showEmbargo}} +
+ {{t 'registries.registrationList.embargoEndDate'}} + + {{t 'registries.registrationList.EmbargoEndPrefix'}} + {{format-date @registration.embargoEndDate}} + +
+ {{else if this.showPublic}} + {{t 'registries.registrationList.public'}} + {{/if}} {{#if (eq @state 'pending_moderation')}} {{else}} @@ -39,4 +50,4 @@ {{/if}}
-{{/if}} +{{/if}} \ No newline at end of file diff --git a/mirage/scenarios/registrations.full.ts b/mirage/scenarios/registrations.full.ts index 1f26a92462d..b0551d729ba 100644 --- a/mirage/scenarios/registrations.full.ts +++ b/mirage/scenarios/registrations.full.ts @@ -133,11 +133,15 @@ function createEgapRegistrationProvider(server: Server, currentUser: ModelInstan server.create('moderator', { id: currentUser.id, user: currentUser, provider: egap }, 'asAdmin'); server.createList('moderator', 5, { provider: egap }); - server.createList('registration', 12, { + server.create('registration', { id: 'embargo-test', title: 'Embargo Tests', provider: egap }, 'isEmbargo'); + server.create('registration', { + id: 'pending-embargo', + title: 'Pending Embargo', reviewsState: RegistrationReviewStates.Pending, + revisionState: RevisionReviewStates.Approved, + embargoEndDate: new Date(2024,0,0), provider: egap, }); - return egap; } diff --git a/translations/en-us.yml b/translations/en-us.yml index 2453b52196b..98e50757072 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1844,6 +1844,9 @@ registries: withdrawn: 'No withdrawn registrations have been found' pending_withdraw: 'No registrations found pending withdrawal' pending_moderation: 'No registrations found with updates pending' + embargoEndDate: 'Embargoed Registration' + EmbargoEndPrefix: 'with an end date of' + public: 'Public Registration' reviewActions: triggerPastTense: submit: 'submitted' From 1150dc69bfeb263d03856ab52369b45300d88a82 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:12:20 -0400 Subject: [PATCH 005/193] ENG-3764 A11y - OSF Home/Landing Page (#2175) - Ticket: [ENG-3764](https://openscience.atlassian.net/browse/ENG-3764) - Feature flag: n/a ## Purpose Fix a11y issue for carousel by removing nested interactive controls ## Summary of Changes - Replaced 'li' elements with 'div' for carousel navigation - Adjusted CSS to apply styles to the new 'div' structure - Updated TypeScript logic to manage active state --- .../addon/components/carousel/component.ts | 18 +-- .../addon/components/carousel/styles.scss | 107 ++++++++++-------- .../addon/components/carousel/template.hbs | 32 +++--- .../components/carousel/x-item/styles.scss | 11 +- .../components/carousel/x-item/template.hbs | 2 +- .../components/carousel/component-test.ts | 8 +- 6 files changed, 93 insertions(+), 85 deletions(-) diff --git a/lib/osf-components/addon/components/carousel/component.ts b/lib/osf-components/addon/components/carousel/component.ts index 535910a68db..bcb9281914c 100644 --- a/lib/osf-components/addon/components/carousel/component.ts +++ b/lib/osf-components/addon/components/carousel/component.ts @@ -22,18 +22,18 @@ export default class Carousel extends Component { @action changeSlide(direction: string) { - const activeSlide = this.carouselItems.findBy('active'); - const activeIndex = activeSlide!.index; - let newIndex = direction === 'previous' ? activeIndex - 1 : activeIndex + 1; + const activeIndex = this.carouselItems.findIndex(item => item.isActive); + let newIndex = activeIndex; - if (newIndex > this.carouselItems.length - 1) { - newIndex = 0; - } else if (newIndex < 0) { - newIndex = this.carouselItems.length - 1; + if (direction === 'previous') { + newIndex = activeIndex - 1 < 0 ? this.carouselItems.length - 1 : activeIndex - 1; + } else if (direction === 'next') { + newIndex = activeIndex + 1 >= this.carouselItems.length ? 0 : activeIndex + 1; } - this.carouselItems[activeIndex].set('isActive', false); - this.carouselItems[newIndex].set('isActive', true); + this.carouselItems.forEach((item, index) => { + item.set('isActive', index === newIndex); + }); } @action diff --git a/lib/osf-components/addon/components/carousel/styles.scss b/lib/osf-components/addon/components/carousel/styles.scss index 73d4ff9bb30..ecccdd1d33f 100644 --- a/lib/osf-components/addon/components/carousel/styles.scss +++ b/lib/osf-components/addon/components/carousel/styles.scss @@ -1,66 +1,73 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + .carousel-container { position: relative; -} -.dot-nav { - margin: 0; - padding: 0; - list-style: none; - text-align: center; - li { - display: inline-block; - margin: 0 5px; - width: 16px; - height: 16px; + .dot-nav { + margin: 0; + padding: 0; + text-align: center; + + + .dot-nav-item { + display: inline-block; + margin: 0 5px; + width: 16px; + height: 16px; + background-color: #a5b3bd; + border-radius: 50%; + transition: background-color 0.1s; + cursor: pointer; + text-indent: -999px; + overflow: hidden; + + + &.current, + &:hover { + background-color: #fff; + box-shadow: inset 0 0 0 2px #263947; + } + } } - button { - left: 0; + .item-list { width: 100%; - height: 100%; - border: 0; - border-radius: 50%; - background-color: #a5b3bd; - text-indent: -999em; - transition: background-color 0.1s; - } + padding: 0 15px; - .current button, - button:hover { - background-color: #fff; - box-shadow: inset 0 0 0 2px #263947; } -} -.item-list { - width: 100%; - padding: 0 15px; - - li { - display: none; - height: inherit; - animation-name: fade; - animation-duration: 1s; + .btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: inherit; + border: 0; + cursor: pointer; } -} -.arrow-previous { - transform: rotate(180deg); -} + .arrow-previous { + left: 0; + transform: rotate(180deg); + } -.btn { - position: absolute; - top: 50%; - background: inherit; - border: 0; -} + .btn-next { + right: 0; + } -.btn-next { - right: 0; -} + .visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + } -@keyframes fade { - from { opacity: 0.4; } - to { opacity: 1; } + @keyframes fade { + from { opacity: 0.4; } + to { opacity: 1; } + } } diff --git a/lib/osf-components/addon/components/carousel/template.hbs b/lib/osf-components/addon/components/carousel/template.hbs index 9470c1b01a5..4704dd77d47 100644 --- a/lib/osf-components/addon/components/carousel/template.hbs +++ b/lib/osf-components/addon/components/carousel/template.hbs @@ -5,32 +5,26 @@ ...attributes > {{yield (hash header=(component 'carousel/x-header'))}} -
    +
    {{#each this.carouselItems as |item|}} -
  1. - -
  2. + {{t 'osf-components.carousel.go_to_slide' slideIndex=item.slideIndex}} + {{#if item.isActive}} + {{t 'osf-components.carousel.current_slide'}} + {{/if}} +
    {{/each}} -
-
    +
+
{{yield (hash slide=(component 'carousel/x-item' register=(action this.register) allItems=this.carouselItems))}} - +
+
+ + {{concat + +
+
+ + {{concat + +
diff --git a/lib/registries/public/assets/img/provider_logos/GFS_Logo.png b/lib/registries/public/assets/img/provider_logos/GFS_Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9664d766637faa3c5a5cb412a78101db975196f3 GIT binary patch literal 46034 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

X_Caqu%LLhK+TIF9gLY7ORuU#I;W$_=8@Cb;dsw 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 div').exists({count: 4}); - assert.dom('[data-test-registries-list-row-two] > div').exists({count: 3}); + assert.dom('[data-test-registries-list-row-two] > div').exists({count: 5}); assert.dom('[data-test-registries-list-contact-link]') .hasAttribute('href', @@ -288,4 +288,82 @@ module('Integration | Component | registries | registries-services-list', hooks 'YOUth Study Registrylogo', 'The image alt tag is correct.'); }); + + test('the registries services list TWCF Consciousness Registry', + async function( + this: EnginesIntlTestContext, + assert, + ) { + // Given the component is rendered + await render(hbs``, { owner: this.engine }); + + // Given I find the node + const node = document.querySelector('[data-test-twcf-consciousness-registry]'); + + // Then I validate the link + assert.dom(node) + .hasAttribute('href', + 'https://osf.io/registries/twcfconscious', + 'The a href link is correct.'); + + // And I validate the link aria-label + assert.dom(node) + .hasAttribute('aria-label', + 'TWCF Consciousness Registry', + 'The a aria-label is correct.'); + + // And I validate the data analytics name + assert.dom(node) + .hasAttribute('data-analytics-name', + 'TWCF Consciousness - Registry', + 'The data-analytics-name attribute is correct.'); + + // Given I find the image node + const imageNode = document.querySelector('[data-test-twcf-consciousness-registry-logo]'); + + // And I validate the image alt tag + assert.dom(imageNode) + .hasAttribute('alt', + 'TWCF Consciousness Registrylogo', + 'The image alt tag is correct.'); + }); + + test('the registries services list GFS Registry', + async function( + this: EnginesIntlTestContext, + assert, + ) { + // Given the component is rendered + await render(hbs``, { owner: this.engine }); + + // Given I find the node + const node = document.querySelector('[data-test-gfs-registry]'); + + // Then I validate the link + assert.dom(node) + .hasAttribute('href', + 'https://osf.io/registries/gfs', + 'The a href link is correct.'); + + // And I validate the link aria-label + assert.dom(node) + .hasAttribute('aria-label', + 'Global Flourishing Study Registry', + 'The a aria-label is correct.'); + + // And I validate the data analytics name + assert.dom(node) + .hasAttribute('data-analytics-name', + 'GFS - Registry', + 'The data-analytics-name attribute is correct.'); + + // Given I find the image node + const imageNode = document.querySelector('[data-test-gfs-registry-logo]'); + + // And I validate the image alt tag + assert.dom(imageNode) + .hasAttribute('alt', + 'Global Flourishing Study Registrylogo', + 'The image alt tag is correct.'); + }); }); diff --git a/translations/en-us.yml b/translations/en-us.yml index 98e50757072..bd6cbdbcd47 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1549,6 +1549,8 @@ registries: rwe: 'Real World Evidence Registry' youth: 'YOUth Study Registry' prereg: 'Preregistration Challenge' + twcfconsciousness: 'TWCF Consciousness Registry' + gfs: 'Global Flourishing Study Registry' logo: logo recent: title: 'Browse Registrations' From e24da94cb81c13f28b1a002acd77dae5750d9306 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Tue, 9 Apr 2024 14:44:38 -0400 Subject: [PATCH 008/193] Revert "disable CEDAR embeddable editor and artifact viewer" This reverts commit 43f5e9fe1deb0f4db9b51bce5d4e52135510da0d. --- app/router.ts | 4 ++-- ember-cli-build.js | 4 ++-- .../components/metadata/metadata-detail/template.hbs | 4 ++-- lib/registries/addon/routes.ts | 2 +- package.json | 2 ++ yarn.lock | 10 ++++++++++ 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/router.ts b/app/router.ts index c1996a16160..3af5e49cc9f 100644 --- a/app/router.ts +++ b/app/router.ts @@ -67,7 +67,7 @@ Router.map(function() { this.route('guid-file', { path: '--file/:guid' }, function() { this.route('index', { path: '/'}); this.route('metadata', function() { - // this.route('add'); + this.route('add'); }); }); @@ -80,7 +80,7 @@ Router.map(function() { this.route('metadata', function() { this.route('index', { path: '/'}); this.route('detail', { path: '/:recordId' }); - // this.route('add'); + this.route('add'); }); this.route('registrations'); this.route('drafts', { path: '/drafts/:draftId' }, function() { diff --git a/ember-cli-build.js b/ember-cli-build.js index 6c1da84ccf3..ebbc8fe6086 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -113,7 +113,7 @@ module.exports = function(defaults) { app.import('node_modules/dropzone/dist/dropzone.js'); app.import('node_modules/wicg-inert/dist/inert.min.js'); - // app.import('node_modules/cedar-embeddable-editor/cedar-embeddable-editor.js'); - // app.import('node_modules/cedar-artifact-viewer/cedar-artifact-viewer.js'); + app.import('node_modules/cedar-embeddable-editor/cedar-embeddable-editor.js'); + app.import('node_modules/cedar-artifact-viewer/cedar-artifact-viewer.js'); return app.toTree(); }; diff --git a/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs b/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs index 61ec2f8e61c..63e73febc39 100644 --- a/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs +++ b/lib/osf-components/addon/components/metadata/metadata-detail/template.hbs @@ -6,7 +6,7 @@

- {{!-- {{#if this.hasWritePermission}} + {{#if this.hasWritePermission}}
@@ -29,7 +29,7 @@ {{/if}}
- {{/if}} --}} + {{/if}}
diff --git a/lib/registries/addon/routes.ts b/lib/registries/addon/routes.ts index da39826bcec..3b9c9d71c86 100644 --- a/lib/registries/addon/routes.ts +++ b/lib/registries/addon/routes.ts @@ -47,7 +47,7 @@ export default buildRoutes(function() { this.route('metadata', function() { this.route('index', { path: '/'}); this.route('detail', { path: '/:recordId' }); - // this.route('add'); + this.route('add'); }); }); diff --git a/package.json b/package.json index 0ab14014d18..f3b68ff9f06 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,8 @@ "babel-eslint": "^8.0.0", "broccoli-asset-rev": "^3.0.0", "c3": "^0.6.14", + "cedar-artifact-viewer": "^0.9.3", + "cedar-embeddable-editor": "^1.2.1", "chai": "^4.1.2", "coveralls": "^3.0.3", "dropzone": "5.5.1", diff --git a/yarn.lock b/yarn.lock index 7ffd33553ae..4eebcbe6e88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9165,6 +9165,16 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== +cedar-artifact-viewer@^0.9.3: + version "0.9.5" + resolved "https://registry.yarnpkg.com/cedar-artifact-viewer/-/cedar-artifact-viewer-0.9.5.tgz#0679b8cff2a2be96c0ef9ac8cc567327aba6e2e6" + integrity sha512-o23pXLrLBB6ZgZZW79SaE+c41CEGSASZ9YC0qKd8BK8b2EmLwiH18dEQv5pXYSxKKo3Ue7WdnyLoRNEZ+yo9mQ== + +cedar-embeddable-editor@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cedar-embeddable-editor/-/cedar-embeddable-editor-1.2.1.tgz#e4a27e657b6d63fd4e86506ed4ca77a693b8c51d" + integrity sha512-XyAgvQimvFBCuLuWIvYpb2fEgCKMgEOj9vHP47Heurycs9B6Wbvk3bIvJ8GbqadKA8j83z1772vF3f+65P89qw== + chai-as-promised@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-6.0.0.tgz#1a02a433a6f24dafac63b9c96fa1684db1aa8da6" From 24feb684ff31978c292ae2849c30b0492d7eca9f Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:54:53 -0400 Subject: [PATCH 009/193] ENG-4525 Update guid-file and guid-node/metadata page titles to include file name/project name (#2192) - Ticket: [ENG-4525](https://openscience.atlassian.net/browse/ENG-4525) - Feature flag: n/a ## Purpose Update guid-file and guid-node/metadata page titles to include file name/project name ## Summary of Changes - Modified the analytics charts to display titles from the API instead of GUIDs - Removed aggregation logic for chart data processing Co-authored-by: Uditi Mehta --- .../addon/components/analytics-charts/component.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/analytics-page/addon/components/analytics-charts/component.ts b/lib/analytics-page/addon/components/analytics-charts/component.ts index ad713052ae5..334cfb75369 100644 --- a/lib/analytics-page/addon/components/analytics-charts/component.ts +++ b/lib/analytics-page/addon/components/analytics-charts/component.ts @@ -298,11 +298,13 @@ export default class AnalyticsChart extends Component { } else { const data: PopularPageDatum[] = this.args.chartsDataTaskInstance.value!.popular_pages as any; const aggregatedResults: { [name: string]: PopularPageDisplay } = {}; + data.forEach(datum => { - const displayDatum = this.popularPageDisplay(datum); - const priorDisplay = aggregatedResults[displayDatum.name]; - if (priorDisplay) { - priorDisplay.count += displayDatum.count; + const cleanTitle = datum.title.replace(/^OSF \| /, ''); + const displayDatum = { name: cleanTitle, count: datum.count }; + + if (aggregatedResults[displayDatum.name]) { + aggregatedResults[displayDatum.name].count += displayDatum.count; } else { aggregatedResults[displayDatum.name] = displayDatum; } From 4b3143a8177708ba75b62e649184153631cb9fa9 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 15 Apr 2024 14:51:06 -0400 Subject: [PATCH 010/193] upgrade cee --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f3b68ff9f06..773adb83a10 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "broccoli-asset-rev": "^3.0.0", "c3": "^0.6.14", "cedar-artifact-viewer": "^0.9.3", - "cedar-embeddable-editor": "^1.2.1", + "cedar-embeddable-editor": "1.2.2", "chai": "^4.1.2", "coveralls": "^3.0.3", "dropzone": "5.5.1", diff --git a/yarn.lock b/yarn.lock index 4eebcbe6e88..8d9cd3813ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9170,10 +9170,10 @@ cedar-artifact-viewer@^0.9.3: resolved "https://registry.yarnpkg.com/cedar-artifact-viewer/-/cedar-artifact-viewer-0.9.5.tgz#0679b8cff2a2be96c0ef9ac8cc567327aba6e2e6" integrity sha512-o23pXLrLBB6ZgZZW79SaE+c41CEGSASZ9YC0qKd8BK8b2EmLwiH18dEQv5pXYSxKKo3Ue7WdnyLoRNEZ+yo9mQ== -cedar-embeddable-editor@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cedar-embeddable-editor/-/cedar-embeddable-editor-1.2.1.tgz#e4a27e657b6d63fd4e86506ed4ca77a693b8c51d" - integrity sha512-XyAgvQimvFBCuLuWIvYpb2fEgCKMgEOj9vHP47Heurycs9B6Wbvk3bIvJ8GbqadKA8j83z1772vF3f+65P89qw== +cedar-embeddable-editor@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cedar-embeddable-editor/-/cedar-embeddable-editor-1.2.2.tgz#be6697000b9ab32a9801bcdc05b9825180669a1d" + integrity sha512-GuwFKwz52JRl0ZwJMWl/cHn2gj5jtP9b8QMsYv8ulQClTUNn6FCDPWsQmBfPAhG+3NMtT3+jkm169C36itJmTA== chai-as-promised@^6.0.0: version "6.0.0" From 7665e3cf7abac41a8b767e58a2ac1d3df8426e30 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Mon, 29 Apr 2024 13:24:57 -0400 Subject: [PATCH 011/193] ENG-3769: Fix a11y issue on draft registration pages (#2199) ## Purpose Fix a11y of an SVG icon on draft registration pages ## Summary of Changes - Changed @ariaHidden to ensures the SVG icon is not hidden. - Added aria-label. Co-authored-by: Uditi Mehta --- .../schema-block-renderer/helper-text-icon/template.hbs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/helper-text-icon/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/helper-text-icon/template.hbs index 38d8cf6d852..d6f0463d625 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/helper-text-icon/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/helper-text-icon/template.hbs @@ -5,7 +5,8 @@ tabindex='0' id={{id}} @icon='question-circle' - @ariaHidden={{false}} + aria-hidden='false' + aria-label={{this.helpText}} ...attributes /> Date: Tue, 30 Apr 2024 15:32:06 -0400 Subject: [PATCH 012/193] Bump version no. Add CHANGELOG --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f932b93e5..4cd9dddac85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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.04.0] - 2024-04-30 +### Added +- Misc bug and a11y fixes + ## [24.03.0] - 2024-02-26 ### Added - Integrate Cedar Embeddable Editor for adding and editing metadata diff --git a/package.json b/package.json index 773adb83a10..7f6db4367bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.03.0", + "version": "24.04.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", From a0aaa10cef34918175cfb8a134e69e97671e0f45 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 9 May 2024 14:08:55 -0400 Subject: [PATCH 013/193] ENG-4839 Implement Visibility Control for Preprint Providers on Discover Page (#2210) ## Purpose Implement Visibility Control for Preprint Providers on Discover Page ## Summary of Changes - Added a new attribute to track whether a preprint provider should be advertised on the discover page. - Integrated the advertiseOnDiscoverPage attribute in the condition to render preprint provider logos. --------- Co-authored-by: Uditi Mehta --- app/models/preprint-provider.ts | 2 ++ app/preprints/index/route.ts | 5 ++++- app/preprints/index/template.hbs | 18 ++++++++---------- mirage/fixtures/preprint-providers.ts | 12 ++++++++++++ tests/acceptance/preprints/index-test.ts | 1 + 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/models/preprint-provider.ts b/app/models/preprint-provider.ts index 22ab4abf0ea..585513cfe77 100644 --- a/app/models/preprint-provider.ts +++ b/app/models/preprint-provider.ts @@ -24,6 +24,8 @@ export default class PreprintProviderModel extends ProviderModel { @attr('string') shareSource!: string; @attr('string') preprintWord!: PreprintWord; + @attr('boolean', { defaultValue: true }) advertiseOnDiscoverPage!: boolean; + // Reviews settings @attr('array') permissions!: ReviewPermissions[]; @attr('boolean', { allowNull: true }) reviewsCommentsPrivate!: boolean | null; diff --git a/app/preprints/index/route.ts b/app/preprints/index/route.ts index e3382f7ff7e..404bce07412 100644 --- a/app/preprints/index/route.ts +++ b/app/preprints/index/route.ts @@ -48,7 +48,10 @@ export default class Preprints extends Route { let brandedProviders = []; if (this.theme.id === 'osf') { - const allProviders = await this.store.findAll('preprint-provider', { reload: true }); + const allProviders = await this.store.query('preprint-provider', { + reload: true, + filter: { advertise_on_discover_page: true }, + }); brandedProviders = allProviders.filter(item => item.id !== 'osf'); } diff --git a/app/preprints/index/template.hbs b/app/preprints/index/template.hbs index e4cb3f777d5..216f45a12ee 100644 --- a/app/preprints/index/template.hbs +++ b/app/preprints/index/template.hbs @@ -96,16 +96,14 @@

{{#each this.model.brandedProviders as |provider| }} - {{#if (not-eq provider.id 'livedata') }} - - {{/if}} + {{/each}}
diff --git a/mirage/fixtures/preprint-providers.ts b/mirage/fixtures/preprint-providers.ts index 5aafd4d31b3..e15de80a363 100644 --- a/mirage/fixtures/preprint-providers.ts +++ b/mirage/fixtures/preprint-providers.ts @@ -16,6 +16,7 @@ const preprintProviders: Array> = [ { id: 'osf', name: 'Open Science Framework', + advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(1), footerLinks: 'fake footer links', @@ -25,6 +26,7 @@ const preprintProviders: Array> = [ { id: 'thesiscommons', name: 'Thesis Commons', + advertiseOnDiscoverPage: true, preprintWord: 'thesis', assets: randomAssets(2), // eslint-disable-next-line max-len @@ -33,6 +35,7 @@ const preprintProviders: Array> = [ { id: 'preprintrxiv', name: 'PreprintrXiv', + advertiseOnDiscoverPage: false, preprintWord: 'preprint', assets: randomAssets(3), // eslint-disable-next-line max-len @@ -43,6 +46,7 @@ const preprintProviders: Array> = [ { id: 'paperxiv', name: 'PaperXiv', + advertiseOnDiscoverPage: true, preprintWord: 'paper', assets: randomAssets(4), // eslint-disable-next-line max-len @@ -51,6 +55,7 @@ const preprintProviders: Array> = [ { id: 'thesisrxiv', name: 'ThesisrXiv', + advertiseOnDiscoverPage: true, preprintWord: 'thesis', assets: randomAssets(5), // eslint-disable-next-line max-len @@ -59,6 +64,7 @@ const preprintProviders: Array> = [ { id: 'workrxiv', name: 'WorkrXiv', + advertiseOnDiscoverPage: true, preprintWord: 'work', assets: randomAssets(6), footerLinks: 'fake footer links', @@ -66,6 +72,7 @@ const preprintProviders: Array> = [ { id: 'docrxiv', name: 'DocrXiv', + advertiseOnDiscoverPage: true, preprintWord: 'default', assets: randomAssets(7), footerLinks: 'fake footer links', @@ -73,6 +80,7 @@ const preprintProviders: Array> = [ { id: 'agrixiv', name: 'AgriXiv', + advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(8), reviewsWorkflow: PreprintProviderReviewsWorkFlow.POST_MODERATION, @@ -80,24 +88,28 @@ const preprintProviders: Array> = [ { id: 'biohackrxiv', name: 'BioHackrXiv', + advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(9), }, { id: 'nutrixiv', name: 'NutriXiv', + advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10), }, { id: 'paleorxiv', name: 'PaleoRrxiv', + advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10, false), }, { id: 'sportrxiv', name: 'Sport-Rxiv', + advertiseOnDiscoverPage: true, preprintWord: 'paper', assets: randomAssets(10), }, diff --git a/tests/acceptance/preprints/index-test.ts b/tests/acceptance/preprints/index-test.ts index c686d31a3f8..e4976ab72a6 100644 --- a/tests/acceptance/preprints/index-test.ts +++ b/tests/acceptance/preprints/index-test.ts @@ -22,6 +22,7 @@ module('Acceptance | preprints | index', hooks => { provider.update({ brand, description: 'This is the description for OSF', + advertiseOnDiscoverPage: true, }); this.provider = provider; }); From e949bd9b83f3553317c88782029f95d878e0f311 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:20:16 -0400 Subject: [PATCH 014/193] Add Editable Subjects Widget to Project Metadata Editor (#2227) - Ticket: [ENG-4354] - Feature flag: n/a ## Purpose To integrate an editable subjects widget into the project metadata editor ## Summary of Changes - Added subjectsAcceptable property to the Node model for project-specific subjects. - Updated SubjectManagerComponent to handle nodes without providers. - Modified BrowseManagerComponent to load subjectsAcceptable if the provider is not present. - Integrated the subjects widget into the node metadata form template for projects. --- app/models/node.ts | 2 + .../node-metadata-form/template.hbs | 34 ++++++++-------- .../browse/browse-manager/component.ts | 39 +++++++++++++------ .../components/subjects/manager/component.ts | 16 ++++++-- .../components/subjects/manager/template.hbs | 1 + mirage/config.ts | 2 + mirage/serializers/node.ts | 16 ++++++++ mirage/views/provider-subjects.ts | 2 +- mirage/views/subjects-acceptable.ts | 36 +++++++++++++++++ tests/acceptance/guid-node/metadata-test.ts | 6 +-- 10 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 mirage/views/subjects-acceptable.ts diff --git a/app/models/node.ts b/app/models/node.ts index 50e8d464c50..1e845311d3c 100644 --- a/app/models/node.ts +++ b/app/models/node.ts @@ -120,6 +120,8 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col @attr('boolean') currentUserCanComment!: boolean; @attr('boolean') wikiEnabled!: boolean; + @hasMany('subject', { inverse: null, async: false }) subjectsAcceptable?: SubjectModel[]; + // FE-only property to check enabled addons. // null until getEnabledAddons has been called @tracked addonsEnabled?: string[]; diff --git a/lib/osf-components/addon/components/node-metadata-form/template.hbs b/lib/osf-components/addon/components/node-metadata-form/template.hbs index b3ddc9be6f1..9c60de6fd3d 100644 --- a/lib/osf-components/addon/components/node-metadata-form/template.hbs +++ b/lib/osf-components/addon/components/node-metadata-form/template.hbs @@ -331,25 +331,27 @@ {{/if}} - {{#if @manager.node.isRegistration}} -
-
-
{{t 'osf-components.node-metadata-form.subjects'}}
-
- +
+
+
{{t 'osf-components.node-metadata-form.subjects'}}
+
+ + {{#if subjectsManager.loadingNodeSubjects}} + + {{else}} - -
-
-
- {{/if}} + {{/if}} +
+
+
+
{{t 'osf-components.node-metadata-form.tags'}}
diff --git a/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts b/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts index c3eaa7891c1..e5c744a03f7 100644 --- a/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts +++ b/lib/osf-components/addon/components/subjects/browse/browse-manager/component.ts @@ -42,18 +42,33 @@ export default class SubjectBrowserManagerComponent extends Component { @waitFor async loadRootSubjects() { try { - const provider = await this.subjectsManager.provider; - const rootSubjects = await provider.queryHasMany('subjects', { - filter: { - parent: 'null', - }, - page: { - size: subjectPageSize, - }, - sort: 'text', - related_counts: 'children', - }); - this.setProperties({ rootSubjects }); + if (this.subjectsManager.provider) { + const provider = await this.subjectsManager.provider; + const rootSubjects = await provider.queryHasMany('subjects', { + filter: { + parent: 'null', + }, + page: { + size: subjectPageSize, + }, + sort: 'text', + related_counts: 'children', + }); + this.setProperties({ rootSubjects }); + } else { + const model = this.subjectsManager.model; + const rootSubjects = await model.queryHasMany('subjectsAcceptable', { + filter: { + parent: 'null', + }, + page: { + size: subjectPageSize, + }, + sort: 'text', + related_counts: 'children', + }); + this.setProperties({ rootSubjects }); + } } catch (e) { const errorMessage = this.intl.t('registries.registration_metadata.load_subjects_error'); captureException(e, { errorMessage }); diff --git a/lib/osf-components/addon/components/subjects/manager/component.ts b/lib/osf-components/addon/components/subjects/manager/component.ts index 7b74acd5a7b..7938e99ea2f 100644 --- a/lib/osf-components/addon/components/subjects/manager/component.ts +++ b/lib/osf-components/addon/components/subjects/manager/component.ts @@ -23,6 +23,8 @@ import template from './template'; interface ModelWithSubjects extends OsfModel { subjects: SubjectModel[]; + subjectsAcceptable?: SubjectModel[]; + isProject: boolean; } // SubjectManager is responsible for: @@ -34,7 +36,8 @@ export interface SubjectManager { savedSubjects: SubjectModel[]; isSaving: boolean; hasChanged: boolean; - provider: ProviderModel; + provider?: ProviderModel; + model: ModelWithSubjects; selectSubject(subject: SubjectModel): void; unselectSubject(subject: SubjectModel): void; @@ -51,7 +54,7 @@ export interface SubjectManager { export default class SubjectManagerComponent extends Component { // required model!: ModelWithSubjects; - provider!: ProviderModel; + provider?: ProviderModel; doesAutosave!: boolean; // optional @@ -156,8 +159,15 @@ export default class SubjectManagerComponent extends Component { super.init(); assert('@model is required', Boolean(this.model)); - assert('@provider is required', Boolean(this.provider)); assert('@doesAutosave is required', this.doesAutosave !== null && this.doesAutosave !== undefined); + const isProject = this.model.get('isProject'); + if (!isProject) { + assert('@provider is required', Boolean(this.provider)); + } + + if (isProject) { + assert('@subjectsAcceptable is required', this.model.get('subjectsAcceptable') !== undefined); + } } @action diff --git a/lib/osf-components/addon/components/subjects/manager/template.hbs b/lib/osf-components/addon/components/subjects/manager/template.hbs index b844adfde5c..58b78939ea8 100644 --- a/lib/osf-components/addon/components/subjects/manager/template.hbs +++ b/lib/osf-components/addon/components/subjects/manager/template.hbs @@ -4,6 +4,7 @@ isSaving=this.saveChanges.isRunning hasChanged=this.hasChanged provider=this.provider + model=this.model selectSubject=(action this.selectSubject) unselectSubject=(action this.unselectSubject) diff --git a/mirage/config.ts b/mirage/config.ts index 9187d16672f..32187f3d1e7 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -28,6 +28,7 @@ import { addCollectionModerator, addRegistrationModerator } from './views/modera import { createNode, storageStatus } from './views/node'; import { osfNestedResource, osfResource, osfToManyRelationship } from './views/osf-resource'; import { getProviderSubjects } from './views/provider-subjects'; +import { getSubjectsAcceptable } from './views/subjects-acceptable'; import { createRegistration, forkRegistration, @@ -119,6 +120,7 @@ export default function(this: Server) { osfResource(this, 'subject', { only: ['show'] }); osfNestedResource(this, 'subject', 'children', { only: ['index'] }); osfNestedResource(this, 'node', 'children'); + this.get('/nodes/:parentID/subjectsAcceptable', getSubjectsAcceptable); osfNestedResource(this, 'node', 'contributors', { defaultSortKey: 'index', onCreate: createBibliographicContributor, diff --git a/mirage/serializers/node.ts b/mirage/serializers/node.ts index f40e5ab6ccd..94210ecee73 100644 --- a/mirage/serializers/node.ts +++ b/mirage/serializers/node.ts @@ -141,6 +141,22 @@ export default class NodeSerializer extends ApplicationSerializer { }, }, }, + subjects: { + links: { + related: { + href: `${apiUrl}/v2/nodes/${model.id}/subjects/`, + meta: this.buildRelatedLinkMeta(model, 'subjects'), + }, + }, + }, + subjectsAcceptable: { + links: { + related: { + href: `${apiUrl}/v2/nodes/${model.id}/subjectsAcceptable/`, + meta: this.buildRelatedLinkMeta(model, 'subjectsAcceptable'), + }, + }, + }, }; if (model.attrs.parentId !== null) { const { parentId } = model.attrs; diff --git a/mirage/views/provider-subjects.ts b/mirage/views/provider-subjects.ts index 3a6e79f1f9a..1951f1b3c39 100644 --- a/mirage/views/provider-subjects.ts +++ b/mirage/views/provider-subjects.ts @@ -2,7 +2,7 @@ import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage import Subject from 'ember-osf-web/models/subject'; import { process } from './utils'; -function getFilterOpts( +export function getFilterOpts( queryParams: { [key: string]: string }, ): { type: string, value: string } { if ('filter[parent]' in queryParams) { diff --git a/mirage/views/subjects-acceptable.ts b/mirage/views/subjects-acceptable.ts new file mode 100644 index 00000000000..2e8ec79ee74 --- /dev/null +++ b/mirage/views/subjects-acceptable.ts @@ -0,0 +1,36 @@ +import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage'; +import Subject from 'ember-osf-web/models/subject'; +import { process } from './utils'; +import { getFilterOpts } from './provider-subjects'; + +export function getSubjectsAcceptable(this: HandlerContext, schema: Schema, request: Request) { + const { pageSize } = request.queryParams; + const filterOpts = getFilterOpts(request.queryParams); + + const subjects = schema.subjects.all().models; + let filteredSubjects: Array>; + + if (filterOpts.type === 'parent') { + if (filterOpts.value === 'null') { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => !subject.parent, + ); + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.parent && (subject.parent.id === filterOpts.value), + ); + } + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.text.includes(filterOpts.value), + ); + } + + return process( + schema, + request, + this, + filteredSubjects.map(subject => this.serialize(subject).data), + { defaultPageSize: Number(pageSize) }, + ); +} diff --git a/tests/acceptance/guid-node/metadata-test.ts b/tests/acceptance/guid-node/metadata-test.ts index f20f41fffcb..a28acb332c6 100644 --- a/tests/acceptance/guid-node/metadata-test.ts +++ b/tests/acceptance/guid-node/metadata-test.ts @@ -45,7 +45,7 @@ module('Acceptance | guid-node/metadata', hooks => { .containsText(funder.award_number, `Funder award number is correct for ${funder.funder_name}`); } assert.dom('[data-test-contributors-list]').exists(); - assert.dom('[data-test-subjects-list]').doesNotExist('There are no subjects for projects'); + assert.dom('[data-test-subjects-list]').exists('Subjects list is displayed for projects'); assert.dom('[data-test-edit-node-description-button]').doesNotExist(); assert.dom('[data-test-edit-resource-metadata-button]').doesNotExist(); @@ -86,7 +86,7 @@ module('Acceptance | guid-node/metadata', hooks => { .doesNotExist(`Funder award number does not exist for ${funder.funder_name}`); } assert.dom('[data-test-contributors-list]').doesNotExist('There are no contributors for AVOL'); - assert.dom('[data-test-subjects-list]').doesNotExist('There are no subjects for projects'); + assert.dom('[data-test-subjects-list]').exists('Subjects list is displayed for projects'); assert.dom('[data-test-edit-node-description-button]').doesNotExist(); assert.dom('[data-test-edit-resource-metadata-button]').doesNotExist(); @@ -126,7 +126,7 @@ module('Acceptance | guid-node/metadata', hooks => { .containsText(funder.award_number, `Funder award number is correct for ${funder.funder_name}`); } assert.dom('[data-test-contributors-list]').exists(); - assert.dom('[data-test-subjects-list]').doesNotExist('There are no subjects for projects'); + assert.dom('[data-test-subjects-list]').exists('Subjects list is displayed for projects'); assert.dom('[data-test-edit-node-description-button]').exists(); await click('[data-test-edit-node-description-button]'); From 8b15dbb68814af9c25490aab235e46a76bcc56af Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:25:19 -0400 Subject: [PATCH 015/193] Fix Search on Subject Widget (#2233) ## Purpose Fix Search on Subject Widget ## Summary of Changes Updated lib/osf-components/addon/components/subjects/search/component.ts to handle searching for subjects based on whether the model is a project or not --- .../components/subjects/search/component.ts | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/osf-components/addon/components/subjects/search/component.ts b/lib/osf-components/addon/components/subjects/search/component.ts index 3b78f7a9ee0..9478c0a51de 100644 --- a/lib/osf-components/addon/components/subjects/search/component.ts +++ b/lib/osf-components/addon/components/subjects/search/component.ts @@ -22,7 +22,6 @@ export default class SearchSubjects extends Component { @alias('doSearch.isRunning') isLoading!: boolean; - @alias('doSearch.lastSuccessful.value') searchResults?: SubjectModel[]; @computed('searchResults.[]') @@ -36,24 +35,42 @@ export default class SearchSubjects extends Component { async doSearch() { await timeout(500); // debounce - const provider = await this.subjectsManager.provider; - const { userQuery } = this; if (!userQuery) { return undefined; } - const filterResults = await provider.queryHasMany('subjects', { - filter: { - text: userQuery, - }, - page: { - size: 150, - }, - sort: 'text', - related_counts: 'children', - embed: 'parent', - }); - - return filterResults; + + if (this.subjectsManager.model.get('isProject')) { + const model = this.subjectsManager.model; + const filterResults = await model.queryHasMany('subjectsAcceptable', { + filter: { + text: userQuery, + }, + page: { + size: 150, + }, + sort: 'text', + related_counts: 'children', + }); + + this.set('searchResults', filterResults); + return filterResults; + } else { + const provider = await this.subjectsManager.provider; + const filterResults = await provider.queryHasMany('subjects', { + filter: { + text: userQuery, + }, + page: { + size: 150, + }, + sort: 'text', + related_counts: 'children', + embed: 'parent', + }); + + this.set('searchResults', filterResults); + return filterResults; + } } } From 3a2d8980da9f21ef37940c3415a50c879bd73168 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Fri, 14 Jun 2024 11:04:48 -0400 Subject: [PATCH 016/193] fix URL query param issue (#2237) - Ticket: https://openscience.atlassian.net/browse/ENG-5228 - Feature flag: n/a ## Purpose Fix search text not populating as query parameter and correct filter-value preview counts. ## Summary of Changes - Renamed cardSearchText to q and @query to @cardSearchText in the search page controller and templates. - Adjusted filter-value preview counts to reflect the current search context's result count. --- app/search/controller.ts | 2 +- lib/registries/addon/branded/discover/template.hbs | 2 +- mirage/views/search.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/search/controller.ts b/app/search/controller.ts index 24625559b86..fbfa7ec1f15 100644 --- a/app/search/controller.ts +++ b/app/search/controller.ts @@ -6,7 +6,7 @@ import { } from 'osf-components/components/search-page/component'; export default class SearchController extends Controller { - @tracked cardSearchText?: string = ''; + @tracked q?: string = ''; @tracked sort?: string = '-relevance'; @tracked resourceType?: ResourceTypeFilterValue | null = null; @tracked activeFilters?: Filter[] = []; diff --git a/lib/registries/addon/branded/discover/template.hbs b/lib/registries/addon/branded/discover/template.hbs index 6d6cd0793be..61cae81f6d8 100644 --- a/lib/registries/addon/branded/discover/template.hbs +++ b/lib/registries/addon/branded/discover/template.hbs @@ -9,7 +9,7 @@ @defaultQueryOptions={{this.defaultQueryOptions}} @resourceType={{'Registration,RegistrationComponent'}} @queryParams={{this.queryParams}} - @query={{this.q}} + @cardsearchText={{this.q}} @sort={{this.sort}} @onQueryParamChange={{action this.onQueryParamChange}} @showResourceTypeFilter={{false}} diff --git a/mirage/views/search.ts b/mirage/views/search.ts index 36fafeab240..e26a1e9369d 100644 --- a/mirage/views/search.ts +++ b/mirage/views/search.ts @@ -469,7 +469,7 @@ export function valueSearch(_: Schema, __: Request) { matchingHighlight: 'National Institute of Health', }, ], - cardSearchResultCount: 2134, + cardSearchResultCount: 3, }, relationships: { indexCard: { From 961a036af07fb2dd521be5f8ebbb6b0d66718725 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:59:00 -0400 Subject: [PATCH 017/193] Feature/preprints phase 2 (#2261) * Fix TS variable naming * Implement is-selected style check/listener * Add data-test selector * Clean up css and hbs for mobile * Disable and grey out the create button when no provider is selected * Improve naming for CSS classes * Fix extra spaces in translations/en-us.yml * CR reponse: improve code quality + use +
+ + {{#if this.provider.description}} + {{html-safe this.provider.description}} + {{else}} + {{html-safe this.provider.name}} + {{/if}} + +
diff --git a/app/preprints/-components/preprint-provider-selection/styles.scss b/app/preprints/-components/preprint-provider-selection/styles.scss new file mode 100644 index 00000000000..4d548532d43 --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/styles.scss @@ -0,0 +1,96 @@ +// stylelint-disable max-nesting-depth + +@import 'app/styles/layout'; + +.provider-selection-container { + @include clamp-width; + width: 100%; + display: flex; + padding: 15px 0; + flex-direction: column; + justify-content: center; + align-items: flex-start; + font-style: normal; + + .heading-container { + width: 100%; + padding: 15px 0; + + .heading { + margin: 5px 10px; + font-size: 24px; + font-weight: bold; + } + } + + .paragraph-container { + width: 100%; + padding: 15px 0; + + .paragraph { + margin: 5px 10px; + font-size: 16px; + font-weight: 400; + } + } + + .provider-list-container { + width: 100%; + padding: 15px 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-content: flex-start; + } + + .create-button-container { + width: 100%; + padding: 15px 0; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + .create-button { + width: 175px; + height: 40px; + border-radius: 5px; + background-color: $color-bg-blue-dark; + font-size: 16px; + font-weight: bold; + color: $color-text-white; + border-style: hidden; + + &:hover { + background-color: $color-bg-blue-highlight; + } + + &.disabled { + background-color: $color-bg-gray; + } + } + } + + &.mobile { + min-width: 330px; + + .heading-container { + .heading { + margin: 5px 20px; + font-size: 20px; + } + } + + .paragraph-container { + .paragraph { + margin: 5px 20px; + } + } + + .provider-list-container { + justify-content: center; + align-content: center; + } + } +} diff --git a/app/preprints/-components/preprint-provider-selection/template.hbs b/app/preprints/-components/preprint-provider-selection/template.hbs new file mode 100644 index 00000000000..5cf71c094b0 --- /dev/null +++ b/app/preprints/-components/preprint-provider-selection/template.hbs @@ -0,0 +1,39 @@ +
+
+

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

+
+
+

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ + {{license.name}} + + + {{#if this.displayRequiredLicenseFields}} + + + + {{/if}} + {{/let}} +
+ {{#let (unique-id) 'subjects' as |subjectsFieldId|}} + + + + + + {{/let}} +
+
+ {{#let (unique-id) 'tags' as |tagsFieldId|}} + + + {{/let}} +
+ + + +
+ +
+ + +
+
+ {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/component.ts b/app/preprints/-components/submit/preprint-state-machine/action-flow/component.ts new file mode 100644 index 00000000000..559bb60fd33 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/component.ts @@ -0,0 +1,79 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import { waitFor } from '@ember/test-waiters'; + +/** + * The Action Flow Args + */ +interface ActionFlowArgs { + manager: PreprintStateMachine; +} + +/** + * The Action Flow Component + */ +export default class ActionFlow extends Component{ + @service intl!: Intl; + manager = this.args.manager; + + public get isSubmit(): boolean { + return this.manager.isSelected(this.manager.getReviewType); + } + + /** + * Calls the state machine next method + */ + @action + public onPrevious(): void { + this.manager.onPrevious(); + } + + /** + * Calls the state machine next method + */ + @task + @waitFor + public async onNext(): Promise { + await taskFor(this.manager.onNext).perform(); + } + + /** + * Calls the state machine submit method + */ + @task + @waitFor + public async onSubmit(): Promise { + await taskFor(this.manager.onSubmit).perform(); + } + + /** + * Calls the state machine delete method + */ + @task + @waitFor + public async onDelete(): Promise { + await taskFor(this.manager.onDelete).perform(); + } + + /** + * internationalize the delete modal title + */ + public get modalTitle(): string { + return this.intl.t('preprints.submit.action-flow.delete-modal-title', + { singularPreprintWord: this.manager.provider.documentType.singularCapitalized }); + } + + /** + * internationalize the delete modal body + */ + public get modalBody(): string { + return this.intl.t('preprints.submit.action-flow.delete-modal-body', + { singularPreprintWord: this.manager.provider.documentType.singular}); + } + +} diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss b/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss new file mode 100644 index 00000000000..1a562404190 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/styles.scss @@ -0,0 +1,55 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.action-flow-container { + width: 100%; + height: 200px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + border-bottom: 1px solid $color-border-gray-darker; + + .btn { + width: 145px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-evenly; + + + &.white { + color: $color-text-white; + } + + &.disabled { + color: $color-text-black; + background-color: $color-bg-gray-blue-light; + border: 1px solid transparent; + cursor: default; + } + } + + .mobile-disabled { + color: $color-bg-gray-darker; + cursor: default; + } + + .desktop-button-container { + margin-top: 20px; + } + + .mobile-button-container { + width: 33%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + + &.mobile { + height: fit-content; + flex-direction: row; + height: 40px; + border: 0; + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs b/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs new file mode 100644 index 00000000000..8201e157ba5 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/template.hbs @@ -0,0 +1,128 @@ +
+ {{#if this.isSubmit}} + {{#if (is-mobile)}} +
+ +
+
+ +
+ {{else}} +
+ +
+ {{/if}} + {{else}} + {{#if (is-mobile)}} +
+ +
+
+ +
+ {{else}} +
+ +
+ {{/if}} + {{/if}} + {{#if @manager.isDeleteButtonDisplayed}} + {{#if (is-mobile)}} +
+ +
+ {{else}} +
+ +
+ {{/if}} + {{/if}} + {{#if @manager.isWithdrawalButtonDisplayed}} +
+ +
+ {{/if}} +
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts new file mode 100644 index 00000000000..8bf0b036b0f --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/component.ts @@ -0,0 +1,117 @@ +import Component from '@glimmer/component'; +import { ValidationObject } from 'ember-changeset-validations'; +import { validateLength } from 'ember-changeset-validations/validators'; +import buildChangeset from 'ember-osf-web/utils/build-changeset'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { waitFor } from '@ember/test-waiters'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import config from 'ember-osf-web/config/environment'; +import { PreprintProviderReviewsWorkFlow, ReviewsState } from 'ember-osf-web/models/provider'; +import { SafeString } from '@ember/template/-private/handlebars'; + +const { support: { supportEmail } } = config; + +interface WithdrawalModalArgs { + manager: PreprintStateMachine; +} + +interface WithdrawalFormFields { + withdrawalJustification: string; +} + + +export default class WithdrawalComponent extends Component { + @service intl!: Intl; + @tracked isInvalid = true; + + withdrawalFormValidations: ValidationObject = { + withdrawalJustification: validateLength({ + min: 25, + type: 'greaterThanOrEqualTo', + translationArgs: { + description: this.intl.t('preprints.submit.action-flow.withdrawal-placeholder'), + gte: this.intl.t('preprints.submit.action-flow.withdrawal-input-error'), + }, + }), + }; + + withdrawalFormChangeset = buildChangeset(this.args.manager.preprint, this.withdrawalFormValidations); + + /** + * Calls the state machine delete method + */ + @task + @waitFor + public async onWithdrawal(): Promise { + this.validate(); + if (this.withdrawalFormChangeset.isInvalid) { + return Promise.reject(); + } + this.withdrawalFormChangeset.execute(); + return taskFor(this.args.manager.onWithdrawal).perform(); + } + + @action + public validate(): void { + this.withdrawalFormChangeset.validate(); + this.isInvalid = this.withdrawalFormChangeset.isInvalid; + } + + /** + * internationalize the withdrawal label + */ + public get commentLabel(): string { + return this.intl.t('preprints.submit.action-flow.withdrawal-label'); + } + + /** + * internationalize the modal title + */ + public get modalTitle(): string { + return this.intl.t('preprints.submit.action-flow.withdrawal-modal-title', + { singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized}); + } + + /** + * internationalize the modal explanation + */ + public get modalExplanation(): SafeString { + if (this.args.manager.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION + && this.args.manager.preprint.reviewsState === ReviewsState.PENDING + ) { + return this.intl.t('preprints.submit.action-flow.pre-moderation-notice-pending', + { + singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, + htmlSafe: true, + }) as SafeString; + } else if (this.args.manager.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.PRE_MODERATION + ) { + return this.intl.t('preprints.submit.action-flow.pre-moderation-notice-accepted', + { + singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, + pluralCapitalizedPreprintWord: this.args.manager.provider.documentType.pluralCapitalized, + htmlSafe: true, + }) as SafeString; + } else if (this.args.manager.provider.reviewsWorkflow === PreprintProviderReviewsWorkFlow.POST_MODERATION) { + return this.intl.t('preprints.submit.action-flow.post-moderation-notice', + { + singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, + pluralCapitalizedPreprintWord: this.args.manager.provider.documentType.pluralCapitalized, + htmlSafe: true, + }) as SafeString; + } else { + return this.intl.t('preprints.submit.action-flow.no-moderation-notice', + { + singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized, + pluralCapitalizedPreprintWord: this.args.manager.provider.documentType.pluralCapitalized, + supportEmail, + htmlSafe: true, + }) as SafeString; + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss new file mode 100644 index 00000000000..79a49a58c7e --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/styles.scss @@ -0,0 +1,29 @@ +.btn { + width: 145px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-evenly; +} + +.withdrawal-button { + color: $brand-danger; +} + +.explanation-container { + margin-bottom: 20px; +} + +.form-container { + width: 100%; + + .required { + color: $brand-danger; + } + + .textarea-container { + textarea { + height: 150px; + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs new file mode 100644 index 00000000000..539b9e1f61f --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/action-flow/withdrawal-preprint/template.hbs @@ -0,0 +1,86 @@ + + + {{#if (is-mobile)}} + + {{else}} + + {{/if}} + + + {{this.modalTitle}} + + +
+ {{this.modalExplanation}} +
+
+ + {{#let (unique-id 'comment') as |commentFieldId|}} + + + {{/let}} + +
+
+ + + + +
\ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts new file mode 100644 index 00000000000..5307f4039c6 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -0,0 +1,616 @@ +import Component from '@glimmer/component'; +import PreprintModel, { PreprintDataLinksEnum, PreprintPreregLinksEnum } from 'ember-osf-web/models/preprint'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; +import RouterService from '@ember/routing/router-service'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import Intl from 'ember-intl/services/intl'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import FileModel from 'ember-osf-web/models/file'; +import Toast from 'ember-toastr/services/toast'; +import captureException from 'ember-osf-web/utils/capture-exception'; +import { Permission } from 'ember-osf-web/models/osf-model'; +import { ReviewsState } from 'ember-osf-web/models/provider'; +import { taskFor } from 'ember-concurrency-ts'; + +export enum PreprintStatusTypeEnum { + titleAndAbstract = 'titleAndAbstract', + file = 'file', + metadata = 'metadata', + authorAssertions = 'authorAssertions', + supplements = 'supplements', + review = 'review', +} + +/** + * The State Machine Args + */ +interface StateMachineArgs { + provider: PreprintProviderModel; + preprint: PreprintModel; + setPageDirty: () => void; + resetPageDirty: () => void; +} + +/** + * The Preprint State Machine + */ +export default class PreprintStateMachine extends Component{ + @service store!: Store; + @service router!: RouterService; + @service intl!: Intl; + @service toast!: Toast; + titleAndAbstractValidation = false; + fileValidation = false; + metadataValidation = false; + authorAssertionValidation = false; + supplementValidation = false; + @tracked isNextButtonDisabled = true; + @tracked isPreviousButtonDisabled = true; + @tracked isDeleteButtonDisplayed = false; + @tracked isWithdrawalButtonDisplayed = false; + + provider = this.args.provider; + @tracked preprint: PreprintModel; + displayAuthorAssertions = false; + @tracked statusFlowIndex = 1; + @tracked isEditFlow = false; + + constructor(owner: unknown, args: StateMachineArgs) { + super(owner, args); + + if (this.args.preprint) { + this.preprint = this.args.preprint; + this.setValidationForEditFlow(); + this.isEditFlow = true; + this.isDeleteButtonDisplayed = false; + taskFor(this.canDisplayWitdrawalButton).perform(); + } else { + this.isDeleteButtonDisplayed = true; + this.isWithdrawalButtonDisplayed = false; + this.preprint = this.store.createRecord('preprint', { + provider: this.provider, + }); + } + + this.displayAuthorAssertions = this.provider.assertionsEnabled; + } + + @task + @waitFor + private async canDisplayWitdrawalButton(): Promise { + let isWithdrawalRejected = false; + + const withdrawalRequests = await this.preprint.requests; + const withdrawalRequest = withdrawalRequests.firstObject; + if (withdrawalRequest) { + const requestActions = await withdrawalRequest.queryHasMany('actions', { + sort: '-modified', + }); + + const latestRequestAction = requestActions.firstObject; + // @ts-ignore: ActionTrigger is never + if (latestRequestAction && latestRequestAction.actionTrigger === 'reject') { + isWithdrawalRejected = true; + } + } + + this.isWithdrawalButtonDisplayed = this.preprint.currentUserPermissions.includes(Permission.Admin) && + (this.preprint.reviewsState === ReviewsState.ACCEPTED || + this.preprint.reviewsState === ReviewsState.PENDING) && !isWithdrawalRejected; + + } + + private setValidationForEditFlow(): void { + this.titleAndAbstractValidation = true; + this.fileValidation = true; + this.metadataValidation = true; + this.authorAssertionValidation = true; + this.supplementValidation = true; + this.isNextButtonDisabled = false; + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onDelete(): Promise { + await this.preprint.deleteRecord(); + await this.router.transitionTo('preprints.discover', this.provider.id); + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onWithdrawal(): Promise { + try { + const preprintRequest = await this.store.createRecord('preprint-request', { + comment: this.preprint.withdrawalJustification, + requestType: 'withdrawal', + target: this.preprint, + }); + + await preprintRequest.save(); + + this.toast.success( + this.intl.t('preprints.submit.action-flow.success-withdrawal', + { + singularCapitalizedPreprintWord: this.provider.documentType.singularCapitalized, + }), + ); + + await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); + } catch (e) { + const errorMessage = this.intl.t('preprints.submit.action-flow.error-withdrawal', + { + singularPreprintWord: this.provider.documentType.singular, + }); + this.toast.error(errorMessage); + captureException(e, { errorMessage }); + } + } + + + /** + * saveOnStep + * + * @description Abstracted method to save after each step + */ + private async saveOnStep(): Promise { + try { + await this.preprint.save(); + this.toast.success( + this.intl.t('preprints.submit.action-flow.success', + { + singularPreprintWord: this.provider.documentType.singular, + }), + ); + } catch (e) { + const errorMessage = this.intl.t('preprints.submit.action-flow.error', + { + singularPreprintWord: this.provider.documentType.singular, + }); + this.toast.error(errorMessage); + captureException(e, { errorMessage }); + } + this.statusFlowIndex++; + this.determinePreviousButtonState(); + } + + /** + * determinePreviousButtonState + * + * @description Abstracted method to determine the state of the previous button + * + * @returns void + */ + private determinePreviousButtonState(): void { + this.isPreviousButtonDisabled = this.statusFlowIndex === 1; + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onSubmit(): Promise { + this.args.resetPageDirty(); + if (!this.isEditFlow) { + if (this.provider.reviewsWorkflow) { + const reviewAction = this.store.createRecord('review-action', { + actionTrigger: 'submit', + target: this.preprint, + }); + await reviewAction.save(); + } else { + this.preprint.isPublished = true; + await this.preprint.save(); + } + } + + await this.router.transitionTo('preprints.detail', this.provider.id, this.preprint.id); + } + + /** + * Callback for the action-flow component + */ + @task + @waitFor + public async onNext(): Promise { + if (this.isEditFlow) { + this.args.resetPageDirty(); + } else { + this.args.setPageDirty(); + } + this.isNextButtonDisabled = true; + if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.titleAndAbstract) && + this.titleAndAbstractValidation + ) { + await this.saveOnStep(); + await this.preprint.files; + this.isNextButtonDisabled = !this.metadataValidation; + return; + } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && + this.fileValidation + ) { + await this.saveOnStep(); + this.isNextButtonDisabled = !this.authorAssertionValidation; + return; + } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && + this.metadataValidation + ) { + await this.saveOnStep(); + if (this.displayAuthorAssertions) { + this.isNextButtonDisabled = !this.authorAssertionValidation; + } else { + this.isNextButtonDisabled = !this.supplementValidation; + } + return; + } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.authorAssertions) && + this.authorAssertionValidation + ) { + await this.saveOnStep(); + this.isNextButtonDisabled = !this.supplementValidation; + return; + } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.supplements) && + this.supplementValidation + ) { + await this.saveOnStep(); + return; + } + } + + private setPageDirty(): void { + if (this.isEditFlow) { + this.args.setPageDirty(); + } + } + + /** + * Callback for the action-flow component + */ + @action + public validateTitleAndAbstract(valid: boolean): void { + this.titleAndAbstractValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateFile(valid: boolean): void { + this.fileValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateMetadata(valid: boolean): void { + this.metadataValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateAuthorAssertions(valid: boolean): void { + if (this.preprint.hasCoi === false) { + this.preprint.conflictOfInterestStatement = null; + } + if (this.preprint.hasDataLinks === PreprintDataLinksEnum.NOT_APPLICABLE) { + this.preprint.whyNoData = null; + } + if (this.preprint.hasPreregLinks === PreprintPreregLinksEnum.NOT_APPLICABLE) { + this.preprint.whyNoPrereg = null; + } + this.authorAssertionValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + /** + * Callback for the action-flow component + */ + @action + public validateSupplements(valid: boolean): void { + this.supplementValidation = valid; + this.isNextButtonDisabled = !valid; + this.setPageDirty(); + } + + @action + public onPrevious(): void { + if (this.statusFlowIndex > 1) { + this.statusFlowIndex--; + } + this.determinePreviousButtonState(); + this.isNextButtonDisabled = false; + } + + @action + public onClickStep(type: string): void { + this.isNextButtonDisabled = !this.isFinished(type); + if ( + type === PreprintStatusTypeEnum.titleAndAbstract && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.file && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.metadata && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.authorAssertions && + this.statusFlowIndex > this.getTypeIndex(type) && + this.displayAuthorAssertions + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.supplements && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } else if ( + type === PreprintStatusTypeEnum.review && + this.statusFlowIndex > this.getTypeIndex(type) + ) { + this.statusFlowIndex = this.getTypeIndex(type); + } + + this.determinePreviousButtonState(); + } + + @action + public isSelected(type: string): boolean { + if ( + type === PreprintStatusTypeEnum.titleAndAbstract && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.file && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.metadata && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.authorAssertions && + this.getTypeIndex(type) === this.statusFlowIndex && + this.displayAuthorAssertions + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.supplements && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.review && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else { + return false; + } + } + + @action + public getAnalytics(type: string): string { + return this.intl.t('preprints.submit.data-analytics', {statusType: this.getStatusTitle(type) } ); + } + + + @action + public isDisabled(type: string): boolean { + if ( + type === PreprintStatusTypeEnum.titleAndAbstract && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.file && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.metadata && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.authorAssertions && + this.getTypeIndex(type) === this.statusFlowIndex && + this.displayAuthorAssertions + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.supplements && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else if ( + type === PreprintStatusTypeEnum.review && + this.getTypeIndex(type) === this.statusFlowIndex + ) { + return true; + } else { + return false; + } + } + + private getTypeIndex(type: string): number { + if (type === PreprintStatusTypeEnum.titleAndAbstract) { + return 1; + } else if (type === PreprintStatusTypeEnum.file) { + return 2; + } else if (type === PreprintStatusTypeEnum.metadata) { + return 3; + } else if (type === PreprintStatusTypeEnum.authorAssertions) { + return 4; + } else if (type === PreprintStatusTypeEnum.supplements && this.displayAuthorAssertions) { + return 5; + } else if (type === PreprintStatusTypeEnum.supplements && !this.displayAuthorAssertions) { + return 4; + } else if (type === PreprintStatusTypeEnum.review && this.displayAuthorAssertions) { + return 6; + } else if (type === PreprintStatusTypeEnum.review && !this.displayAuthorAssertions) { + return 5; + } else { + return 0; + } + } + + @action + public isFinished(type: string): boolean { + if (this.displayAuthorAssertions && this.statusFlowIndex > this.getTypeIndex(type)) { + return true; + } else if (!this.displayAuthorAssertions && this.statusFlowIndex > this.getTypeIndex(type)) { + return true; + } else if (this.statusFlowIndex > this.getTypeIndex(type)) { + return true; + } else { + return false; + } + } + + @action + public getStatusTitle(type: string): string { + switch (type) { + case PreprintStatusTypeEnum.titleAndAbstract: + return this.intl.t('preprints.submit.status-flow.step-title-and-abstract'); + case PreprintStatusTypeEnum.file: + return this.intl.t('preprints.submit.status-flow.step-file'); + case PreprintStatusTypeEnum.metadata: + return this.intl.t('preprints.submit.status-flow.step-metadata'); + case PreprintStatusTypeEnum.authorAssertions: + return this.intl.t('preprints.submit.status-flow.step-author-assertions'); + case PreprintStatusTypeEnum.supplements: + return this.intl.t('preprints.submit.status-flow.step-supplements'); + case PreprintStatusTypeEnum.review: + return this.intl.t('preprints.submit.status-flow.step-review'); + default: + return ''; + } + } + + @action + public getFaIcon(type: string): string { + if (this.isSelected(type)) { + return 'dot-circle'; + } else if (this.isFinished(type)) { + return 'check-circle'; + } else { + return 'circle'; + } + } + + /** + * shoulddisplayStatusType + * + * @description Determines if the status type should be displayed + * + * @returns boolean + */ + public shouldDisplayStatusType(type: string): boolean{ + return type === PreprintStatusTypeEnum.authorAssertions ? this.displayAuthorAssertions : true; + } + + /** + * getTitleAndAbstractType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getTitleAndAbstractType(): string { + return PreprintStatusTypeEnum.titleAndAbstract; + } + + /** + * getFileType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getFileType(): string { + return PreprintStatusTypeEnum.file; + } + + /** + * getMetadataType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getMetadataType(): string { + return PreprintStatusTypeEnum.metadata; + } + + /** + * getAuthorAssertionsType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getAuthorAssertionsType(): string { + return PreprintStatusTypeEnum.authorAssertions; + } + + /** + * getSupplementsType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getSupplementsType(): string { + return PreprintStatusTypeEnum.supplements; + } + + /** + * getReviewType + * + * @description Provides the enum type to limit strings in the hbs files + * + * @returns strings + */ + public get getReviewType(): string { + return PreprintStatusTypeEnum.review; + } + + @task + @waitFor + public async addProjectFile(file: FileModel): Promise{ + await file.copy(this.preprint, '/', 'osfstorage'); + const theFiles = await this.preprint.files; + const rootFolder = await theFiles.firstObject!.rootFolder; + const primaryFile = await rootFolder!.files; + this.preprint.set('primaryFile', primaryFile.firstObject); + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/component.ts b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/component.ts new file mode 100644 index 00000000000..b7599719046 --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/component.ts @@ -0,0 +1,69 @@ +import Component from '@glimmer/component'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; + +/** + * The Status Flow Display Args + */ +interface StatusFlowDisplayArgs { + manager: PreprintStateMachine; + isDisplayMobileMenu: boolean; + leftNavToggle: () => void; + type: string; +} + +export default class StatusFlowDisplay extends Component{ + + type = this.args.type; + + private get manager(): PreprintStateMachine { + return this.args.manager; + } + + public get shouldDisplayStatusType(): boolean { + let isDisplay = this.manager.shouldDisplayStatusType(this.type); + if (this.args.isDisplayMobileMenu) { + isDisplay &&= this.isSelected; + } + + return isDisplay; + } + + public get getStatusTitle(): string { + return this.manager.getStatusTitle(this.type); + } + + public get isSelected(): boolean { + return this.manager.isSelected(this.type); + } + + public get isFinished(): boolean { + return this.manager.isFinished(this.type); + } + + public get isDisabled(): boolean { + return this.manager.isDisabled(this.type); + } + + public get getAnalytics(): string { + return this.manager.getAnalytics(this.type); + } + + public get getFaIcon(): string { + return this.args.manager.getFaIcon(this.type); + } + + public onClick(): void { + if (!this.args.isDisplayMobileMenu) { + this.args.leftNavToggle(); + } + this.args.manager.onClickStep(this.type); + } + + public get getLinkClass(): string { + if (this.isSelected) { + return 'selected'; + } else { + return 'unfinished'; + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss new file mode 100644 index 00000000000..60fe628658c --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss @@ -0,0 +1,72 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.status-container { + width: 192px; + height: 40px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + background-color: inherit; + z-index: 1; + + &.cursor { + cursor: pointer; + } + + &.selected { + width: 193px; + margin-right: -1px; + border: 1px solid $color-border-gray-darker; + border-right: 0; + background-color: $color-bg-white; + } + + .graphics-container { + width: 25px; + height: 25px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: $color-bg-white; + + .dot-circle { + color: $color-bg-black; + } + + .check-circle { + color: $brand-success; + } + + .circle { + color: $color-bg-gray; + }; + } + + .link-container { + padding-left: 10px; + width: 100%; + height: 25px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + .btn { + font-weight: bold; + } + + .finished { + color: $brand-success; + } + } + + &.mobile { + width: 100%; + + &.selected { + border: 0; + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs new file mode 100644 index 00000000000..e6ea2e99ebe --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs @@ -0,0 +1,43 @@ +{{#if this.shouldDisplayStatusType}} +
+
+ {{#if @isDisplayMobileMenu}} + + {{else}} + + {{/if}} +
+
+ {{#if this.isFinished}} + + {{else}} +
+ {{ this.getStatusTitle }} +
+ {{/if}} +
+
+{{/if}} \ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss b/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss new file mode 100644 index 00000000000..ed405d15eec --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/styles.scss @@ -0,0 +1,37 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.status-flow-container { + width: 205px; + padding-top: 10px; + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-start; + + .line { + position: absolute; + border-left: 3px solid $color-border-gray; + width: 0; + top: 20px; + left: 22.5px; + z-index: 0; + height: 140px; + + &.long { + height: 175px; + } + } + + &.mobile { + padding-top: 0; + width: 100%; + + .line { + display: none; + + &.long { + height: fit-content; + } + } + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs b/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs new file mode 100644 index 00000000000..0633acae74f --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/template.hbs @@ -0,0 +1,45 @@ +
{{/if}} -
- - - - - - - - - - -
+ {{#unless this.hideCategories}} +
+ + + + + + + + + + +
+ {{/unless}} {{#if @extendedFields}} {{#unless this.registration.isAnonymous}} diff --git a/tests/engines/registries/acceptance/overview/overview-test.ts b/tests/engines/registries/acceptance/overview/overview-test.ts index 04904ddc5da..9f599542bcd 100644 --- a/tests/engines/registries/acceptance/overview/overview-test.ts +++ b/tests/engines/registries/acceptance/overview/overview-test.ts @@ -1,4 +1,3 @@ -import { capitalize } from '@ember/string'; import { click as untrackedClick, fillIn, settled, triggerKeyEvent } from '@ember/test-helpers'; import { ModelInstance } from 'ember-cli-mirage'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -322,7 +321,7 @@ module('Registries | Acceptance | overview.overview', hooks => { assert.dom('[data-test-edit-button="description"]').isNotVisible(); }); - test('editable registration category', async function(assert) { + test('categories section is hidden on registration page', async function(assert) { const reg = server.create('registration', { currentUserPermissions: Object.values(Permission), category: NodeCategory.Project, @@ -330,30 +329,14 @@ module('Registries | Acceptance | overview.overview', hooks => { await visit(`/${reg.id}/`); - await click('[data-test-edit-button="category"]'); - assert.dom('[data-test-select-category] div[class~="ember-power-select-trigger"]') - .hasText(capitalize(reg.category)); - - await untrackedClick('[data-test-select-category] div[class~="ember-power-select-trigger"]'); - assert.dom('.ember-power-select-option').exists({ count: Object.values(NodeCategory).length - 1 }); - - await selectChoose('[data-test-select-category]', capitalize(NodeCategory.Instrumentation)); - await click('[data-test-save-edits]'); - - reg.reload(); - assert.equal(reg.category, NodeCategory.Instrumentation); - - // Read user cannot edit - reg.update({ currentUserPermissions: [Permission.Read] }); - - await visit(`/${reg.id}/`); assert.dom('[data-test-edit-button="category"]').doesNotExist(); + assert.dom('[data-test-select-category]').doesNotExist(); - // Write user can edit reg.update({ currentUserPermissions: [Permission.Read, Permission.Write] }); await visit(`/${reg.id}/`); - assert.dom('[data-test-edit-button="category"]').exists(); + assert.dom('[data-test-edit-button="category"]').doesNotExist(); + assert.dom('[data-test-select-category]').doesNotExist(); }); test('editable publication doi', async function(assert) { From e739942929044630ab8885d053fd4761922e95d2 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:12:42 -0400 Subject: [PATCH 052/193] revert changes to the my-preprint link on the nav bar (#2357) Co-authored-by: Uditi Mehta --- lib/app-components/addon/components/branded-navbar/template.hbs | 2 +- .../addon/components/osf-navbar/preprint-links/template.hbs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/app-components/addon/components/branded-navbar/template.hbs b/lib/app-components/addon/components/branded-navbar/template.hbs index ce625d54020..02d542fe42b 100644 --- a/lib/app-components/addon/components/branded-navbar/template.hbs +++ b/lib/app-components/addon/components/branded-navbar/template.hbs @@ -82,7 +82,7 @@ {{else if (eq this.theme.providerType 'preprint')}}
  • {{t 'navbar.my_preprints'}} diff --git a/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs b/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs index 678af063fb3..46de8b7788e 100644 --- a/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs +++ b/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs @@ -1,6 +1,6 @@
  • From c3aa8c22e486f38606ccc51fc5b5a096180e5889 Mon Sep 17 00:00:00 2001 From: Uditi Mehta <57388785+uditijmehta@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:40:43 -0400 Subject: [PATCH 053/193] [ENG-5879] Hide Categories Section for Draft Registrations (#2360) - Ticket: [https://openscience.atlassian.net/browse/ENG-5879] - Feature flag: n/a ## Purpose Hide Categories Section for Draft Registrations ## Summary of Changes - Added hideCategories Flag - Added Conditional Category Display for draft registrations - Hid node card category icon for registrations --------- Co-authored-by: Uditi Mehta --- .../addon/components/node-card/template.hbs | 4 +++- .../addon/drafts/draft/metadata/controller.ts | 2 ++ .../addon/drafts/draft/metadata/template.hbs | 14 ++++++++------ .../registries/acceptance/branded/new-test.ts | 1 - .../registries/acceptance/draft/draft-test.ts | 16 ---------------- 5 files changed, 13 insertions(+), 24 deletions(-) diff --git a/lib/osf-components/addon/components/node-card/template.hbs b/lib/osf-components/addon/components/node-card/template.hbs index 6abbf9e90b0..59a124100c3 100644 --- a/lib/osf-components/addon/components/node-card/template.hbs +++ b/lib/osf-components/addon/components/node-card/template.hbs @@ -80,7 +80,9 @@ {{/if}} {{/if}} - {{node-card/node-icon category=@node.category}} + {{#unless @node.isRegistration}} + {{node-card/node-icon category=@node.category}} + {{/unless}} {{/if}} {{/let}} - - - + {{#unless this.hideCategories}} + + + + {{/unless}}
    {{t 'registries.registration_metadata.affiliated_institutions'}} diff --git a/tests/engines/registries/acceptance/branded/new-test.ts b/tests/engines/registries/acceptance/branded/new-test.ts index 543c18ad5ac..77446c55ae4 100644 --- a/tests/engines/registries/acceptance/branded/new-test.ts +++ b/tests/engines/registries/acceptance/branded/new-test.ts @@ -114,6 +114,5 @@ module('Registries | Acceptance | branded.new', hooks => { assert.dom('[data-test-contributor-link]').exists({ count: 1 }, 'Only one contributor'); assert.dom('[data-test-contributor-permission]').containsText('Administrator', 'user is admin'); assert.dom('[data-test-contributor-citation]').containsText('Yes', 'user is bibliographic'); - assert.dom('[data-test-option="uncategorized"]').exists('Category is uncategorized by default'); }); }); diff --git a/tests/engines/registries/acceptance/draft/draft-test.ts b/tests/engines/registries/acceptance/draft/draft-test.ts index ed831697f5b..d27c1e590d6 100644 --- a/tests/engines/registries/acceptance/draft/draft-test.ts +++ b/tests/engines/registries/acceptance/draft/draft-test.ts @@ -945,16 +945,6 @@ module('Registries | Acceptance | draft form', hooks => { assert.dom('[data-test-validation-errors="description"]') .exists('error in description appears after removing valid string to blank string'); - // Choose category and add a tag - await click('[data-test-metadata-category] > div'); - await percySnapshot('Registries | Acceptance | draft form | metadata editing | metadata: categories opened'); - assert.dom('[data-option-index="1"]').containsText('Other'); - await click('[data-option-index="1"]'); - - await click('[data-test-metadata-tags]'); - await fillIn('[data-test-metadata-tags] input', 'ragtagbag'); - await triggerKeyEvent('[data-test-metadata-tags] input', 'keydown', 'Enter'); - // No errors for nodelicense fields assert.dom('[data-test-validation-errors="subjects"]') .doesNotExist('no error for required fields that user has yet to change: subjects'); @@ -970,12 +960,6 @@ module('Registries | Acceptance | draft form', hooks => { assert.dom('[data-test-validation-errors="subjects"]'); assert.dom('[data-test-validation-errors="license"]'); - // Category and tag added appear on review page - assert.dom('[data-test-review-response="category"]') - .containsText('Other', 'category that was selected in metadata page shows up in review'); - assert.dom('[data-test-tags-widget-tag="ragtagbag"]') - .exists('tag added in metadata shows up in review page'); - // Return to Metadata page and address errors for subjects and license await click('[data-test-link="metadata"]'); From a117ef9cd4c76cbba3e2948adb20e34106f35c23 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Wed, 30 Oct 2024 12:59:51 -0400 Subject: [PATCH 054/193] Update changelog and bump version --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f51bf7eece..0624380c281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ 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.08.0] - 2024-10-30 +### Added +- Hide categories section for draft registrations + ## [24.07.0] - 2024-09-18 ### Added - Preprints Affiliation Project - FE Release diff --git a/package.json b/package.json index cebbe4b5bbc..bda590569a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.07.0", + "version": "24.08.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", From 386b08e14385db74d8f795122a2b2f780ea53af1 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:02:29 -0500 Subject: [PATCH 055/193] [ENG-5024] feature/insti-dash-improv (#2395) * [ENG-6074] Institutional Dashboard Rework - Update the Institutional Dashboard page to now include a Summary, Users, Projects, Registrations and Preprints page - Update the institution-summary-metrics and institution-user models - Add new metadata fields to SHARE models --------- Co-authored-by: John Tordoff Co-authored-by: John Tordoff <> Co-authored-by: Brian Pilati --- app/adapters/share-adapter.ts | 2 +- app/guid-node/registrations/styles.scss | 37 +- .../chart-kpi/component-test.ts | 111 +++ .../chart-kpi-wrapper/chart-kpi/component.ts | 118 +++ .../chart-kpi-wrapper/chart-kpi/styles.scss | 116 +++ .../chart-kpi-wrapper/chart-kpi/template.hbs | 75 ++ .../chart-kpi-wrapper/component-test.ts | 333 ++++++++ .../chart-kpi-wrapper/component.ts | 224 ++++++ .../-components/chart-kpi-wrapper/styles.scss | 29 + .../chart-kpi-wrapper/template.hbs | 14 + .../departments-panel/component.ts | 75 -- .../-components/departments-panel/styles.scss | 16 - .../departments-panel/template.hbs | 20 - .../styles.scss | 54 ++ .../template.hbs | 85 ++ .../institutional-users-list/component.ts | 203 ++++- .../institutional-users-list/styles.scss | 268 ++++++- .../institutional-users-list/template.hbs | 254 ++++-- .../-components/object-list/component-test.ts | 168 ++++ .../-components/object-list/component.ts | 120 +++ .../contributors-field/component.ts | 75 ++ .../contributors-field/template.hbs | 14 + .../object-list/doi-field/component.ts | 15 + .../object-list/doi-field/template.hbs | 5 + .../-components/object-list/styles.scss | 130 ++++ .../-components/object-list/template.hbs | 259 +++++++ .../dashboard/-components/panel/styles.scss | 34 - .../dashboard/-components/panel/template.hbs | 12 - .../-components/projects-panel/component.ts | 47 -- .../-components/projects-panel/styles.scss | 13 - .../-components/projects-panel/template.hbs | 25 - .../total-count-kpi-wrapper/component-test.ts | 118 +++ .../total-count-kpi-wrapper/component.ts | 101 +++ .../total-count-kpi-wrapper/styles.scss | 29 + .../total-count-kpi-wrapper/template.hbs | 14 + .../total-count-kpi/component-test.ts | 64 ++ .../total-count-kpi/styles.scss | 67 ++ .../total-count-kpi/template.hbs | 20 + app/institutions/dashboard/controller.ts | 37 - app/institutions/dashboard/index/styles.scss | 24 + app/institutions/dashboard/index/template.hbs | 16 + .../dashboard/preprints/controller.ts | 78 ++ app/institutions/dashboard/preprints/route.ts | 4 + .../dashboard/preprints/template.hbs | 6 + .../dashboard/projects/controller.ts | 100 +++ app/institutions/dashboard/projects/route.ts | 4 + .../dashboard/projects/template.hbs | 6 + .../dashboard/registrations/controller.ts | 98 +++ .../dashboard/registrations/route.ts | 4 + .../dashboard/registrations/template.hbs | 6 + app/institutions/dashboard/route.ts | 21 +- app/institutions/dashboard/styles.scss | 100 --- app/institutions/dashboard/template.hbs | 61 +- app/institutions/dashboard/users/styles.scss | 3 + app/institutions/dashboard/users/template.hbs | 11 + app/models/index-card.ts | 43 +- app/models/institution-summary-metric.ts | 13 + app/models/institution-user.ts | 16 +- app/models/institution.ts | 1 + app/models/search-result.ts | 94 ++- app/packages/osfmap/jsonld.ts | 37 + app/router.ts | 7 +- app/styles/_components.scss | 35 + app/styles/_variables.scss | 1 + ember-cli-build.js | 4 +- .../list/item/template.hbs | 4 +- .../adjustable-paginator/component.ts | 92 +++ .../adjustable-paginator/styles.scss | 76 ++ .../adjustable-paginator/template.hbs | 140 ++++ .../add-unregistered-modal/template.hbs | 2 +- .../contributors/card/editable/template.hbs | 2 +- .../contributors/card/readonly/template.hbs | 2 +- .../user-search/card/template.hbs | 2 +- .../index-card-searcher/component.ts | 85 ++ .../index-card-searcher/template.hbs | 16 + .../metadata/metadata-tabs/styles.scss | 42 +- .../addon/components/osf-layout/component.ts | 12 +- .../osf-layout/main-column/template.hbs | 2 + .../addon/components/osf-layout/template.hbs | 6 +- .../addon/components/osf-link/component.ts | 1 + .../addon/components/osf-link/styles.scss | 6 + .../addon/components/osf-link/template.hbs | 2 +- .../paginated-list/has-many/component.ts | 1 + .../paginated-list/has-many/template.hbs | 1 + .../paginated-list/layout/component.ts | 1 + .../paginated-list/layout/styles.scss | 4 + .../paginated-list/layout/template.hbs | 30 +- .../search-result-card/component.ts | 18 +- .../addon/components/sort-arrow/component.ts | 29 + .../addon/components/sort-arrow/styles.scss | 31 + .../addon/components/sort-arrow/template.hbs | 17 + .../components/subjects/widget/styles.scss | 37 +- .../adjustable-paginator/component.js | 1 + .../index-card-searcher/component.js | 1 + .../index-card-searcher/template.js | 1 + .../app/components/sort-arrow/component.js | 1 + .../app/components/sort-arrow/template.js | 1 + mirage/config.ts | 6 +- mirage/factories/institution-department.ts | 2 +- .../factories/institution-summary-metric.ts | 23 +- mirage/factories/institution-user.ts | 30 + mirage/factories/institution.ts | 16 +- mirage/scenarios/dashboard.ts | 1 + mirage/utils.ts | 2 +- mirage/views/search.ts | 727 ++++++++++++------ .../acceptance/institutions/dashboard-test.ts | 63 +- .../components/contributors/component-test.ts | 4 +- .../components/sort-arrow/component-test.ts | 86 +++ .../component-test.ts | 60 +- .../-components/panel/component-test.ts | 57 -- translations/en-us.yml | 113 ++- types/ember-cli-chart.d.ts | 19 + 112 files changed, 4928 insertions(+), 1141 deletions(-) create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component-test.ts create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component.ts create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/styles.scss create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/template.hbs create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/component.ts create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/styles.scss create mode 100644 app/institutions/dashboard/-components/chart-kpi-wrapper/template.hbs delete mode 100644 app/institutions/dashboard/-components/departments-panel/component.ts delete mode 100644 app/institutions/dashboard/-components/departments-panel/styles.scss delete mode 100644 app/institutions/dashboard/-components/departments-panel/template.hbs create mode 100644 app/institutions/dashboard/-components/institutional-dashboard-wrapper/styles.scss create mode 100644 app/institutions/dashboard/-components/institutional-dashboard-wrapper/template.hbs create mode 100644 app/institutions/dashboard/-components/object-list/component-test.ts create mode 100644 app/institutions/dashboard/-components/object-list/component.ts create mode 100644 app/institutions/dashboard/-components/object-list/contributors-field/component.ts create mode 100644 app/institutions/dashboard/-components/object-list/contributors-field/template.hbs create mode 100644 app/institutions/dashboard/-components/object-list/doi-field/component.ts create mode 100644 app/institutions/dashboard/-components/object-list/doi-field/template.hbs create mode 100644 app/institutions/dashboard/-components/object-list/styles.scss create mode 100644 app/institutions/dashboard/-components/object-list/template.hbs delete mode 100644 app/institutions/dashboard/-components/panel/styles.scss delete mode 100644 app/institutions/dashboard/-components/panel/template.hbs delete mode 100644 app/institutions/dashboard/-components/projects-panel/component.ts delete mode 100644 app/institutions/dashboard/-components/projects-panel/styles.scss delete mode 100644 app/institutions/dashboard/-components/projects-panel/template.hbs create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss create mode 100644 app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs delete mode 100644 app/institutions/dashboard/controller.ts create mode 100644 app/institutions/dashboard/index/styles.scss create mode 100644 app/institutions/dashboard/index/template.hbs create mode 100644 app/institutions/dashboard/preprints/controller.ts create mode 100644 app/institutions/dashboard/preprints/route.ts create mode 100644 app/institutions/dashboard/preprints/template.hbs create mode 100644 app/institutions/dashboard/projects/controller.ts create mode 100644 app/institutions/dashboard/projects/route.ts create mode 100644 app/institutions/dashboard/projects/template.hbs create mode 100644 app/institutions/dashboard/registrations/controller.ts create mode 100644 app/institutions/dashboard/registrations/route.ts create mode 100644 app/institutions/dashboard/registrations/template.hbs delete mode 100644 app/institutions/dashboard/styles.scss create mode 100644 app/institutions/dashboard/users/styles.scss create mode 100644 app/institutions/dashboard/users/template.hbs create mode 100644 app/packages/osfmap/jsonld.ts create mode 100644 lib/osf-components/addon/components/adjustable-paginator/component.ts create mode 100644 lib/osf-components/addon/components/adjustable-paginator/styles.scss create mode 100644 lib/osf-components/addon/components/adjustable-paginator/template.hbs create mode 100644 lib/osf-components/addon/components/index-card-searcher/component.ts create mode 100644 lib/osf-components/addon/components/index-card-searcher/template.hbs create mode 100644 lib/osf-components/addon/components/sort-arrow/component.ts create mode 100644 lib/osf-components/addon/components/sort-arrow/styles.scss create mode 100644 lib/osf-components/addon/components/sort-arrow/template.hbs create mode 100644 lib/osf-components/app/components/adjustable-paginator/component.js create mode 100644 lib/osf-components/app/components/index-card-searcher/component.js create mode 100644 lib/osf-components/app/components/index-card-searcher/template.js create mode 100644 lib/osf-components/app/components/sort-arrow/component.js create mode 100644 lib/osf-components/app/components/sort-arrow/template.js create mode 100644 tests/integration/components/sort-arrow/component-test.ts delete mode 100644 tests/integration/routes/institutions/dashboard/-components/panel/component-test.ts diff --git a/app/adapters/share-adapter.ts b/app/adapters/share-adapter.ts index 5716e5b5b0a..ec624e2b900 100644 --- a/app/adapters/share-adapter.ts +++ b/app/adapters/share-adapter.ts @@ -5,7 +5,7 @@ const osfUrl = config.OSF.url; export default class ShareAdapter extends JSONAPIAdapter { host = config.OSF.shareBaseUrl.replace(/\/$/, ''); // Remove trailing slash to avoid // in URLs - namespace = 'api/v3'; + namespace = 'trove'; queryRecord(store: any, type: any, query: any) { // check if we aren't serving locally, otherwise add accessService query param to card/value searches diff --git a/app/guid-node/registrations/styles.scss b/app/guid-node/registrations/styles.scss index 01662ce2136..36b839d970c 100644 --- a/app/guid-node/registrations/styles.scss +++ b/app/guid-node/registrations/styles.scss @@ -1,4 +1,5 @@ // stylelint-disable max-nesting-depth, selector-max-compound-selectors +@import 'app/styles/components'; .registration-container { margin: 30px; @@ -19,41 +20,7 @@ /* stylelint-disable selector-no-qualifying-type */ ul.tab-list { - margin-bottom: 10px; - border-bottom: 1px solid #ddd; - box-sizing: border-box; - color: rgb(51, 51, 51); - display: block; - line-height: 20px; - list-style-image: none; - list-style-position: outside; - list-style-type: none; - height: 41px; - padding: 0; - } - - /* stylelint-enable selector-no-qualifying-type */ - .tab-list { - li { - display: block; - position: relative; - margin-bottom: -1px; - float: left; - height: 41px; - padding: 10px 15px; - } - - li:global(.ember-tabs__tab--selected) { - background-color: #f8f8f8; - border-bottom: 2px solid #204762; - } - - li:hover { - border-color: transparent; - text-decoration: none; - background-color: #f8f8f8; - color: var(--primary-color); - } + @include tab-list; } } } diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component-test.ts b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component-test.ts new file mode 100644 index 00000000000..fd2f9bd1cf9 --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component-test.ts @@ -0,0 +1,111 @@ +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { TestContext } from 'ember-test-helpers'; +import { module, test } from 'qunit'; + +module('Integration | institutions | dashboard | -components | chart-kpi', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(function(this: TestContext) { + const data = Object({ + title: 'This is the title', + chartData: [ + Object({ + label: 'a very long data set title that needs to be handled', + total: 100000, + }), + ], + chartType: 'pie', + }); + + this.set('data', data); + }); + + test('it renders the data correctly', async assert => { + + // Given the component is rendered + await render(hbs` + +`); + // Then the chart is verified + assert.dom('[data-test-chart]') + .exists('The test chart exists'); + + // And the title is verified + assert.dom('[data-test-chart-title]') + .hasText('This is the title'); + + assert.dom('[data-test-toggle-icon]') + .hasAttribute('data-icon', 'caret-down'); + + // Finally the expanded data is not visible + assert.dom('[data-test-expansion-data]') + .hasStyle({display: 'none'}); + }); + + test('it renders the expanded data correctly', async assert => { + + // Given the component is rendered + await render(hbs` + +`); + // When I click the expanded icon + await click('[data-test-expand-additional-data]'); + + // Then I verify the icon has changed + assert.dom('[data-test-toggle-icon]') + .hasAttribute('data-icon', 'caret-up'); + + // And the expanded data is visible + assert.dom('[data-test-expansion-data]') + .exists('The expansion data is visible'); + + // And the expanded data position 0 color is verified + assert.dom('[data-test-expanded-color="0"]') + .hasAttribute('style', 'background-color:#00D1FF'); + + // And the expanded data position 0 name is verified + assert.dom('[data-test-expanded-name="0"]') + .hasText('a very long data set title that needs to be handled'); + + // And the expanded data position 0 total is verified + assert.dom('[data-test-expanded-total="0"]') + .hasText('100000'); + }); + + /** + * I need to determine if this is going to be a feature or not + test('it renders the without data correctly', async function(this: EnginesIntlTestContext, assert) { + const data = Object({ + total: 0, + title: 'This is the title', + icon: 'building', + }); + + this.set('data', data); + + + await render(hbs` + +`); + + assert.dom('[data-test-kpi-title]') + .hasText('This is the title'); + assert.dom('[data-test-kpi-data]') + .hasText('No data for institution found.'); + assert.dom('[data-test-kpi-icon]') + .hasAttribute('data-icon', 'building'); + }); + */ +}); diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component.ts b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component.ts new file mode 100644 index 00000000000..252b2978d98 --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/component.ts @@ -0,0 +1,118 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { ChartData, ChartOptions } from 'ember-cli-chart'; +import Intl from 'ember-intl/services/intl'; +// eslint-disable-next-line max-len +import { ChartDataModel, KpiChartModel } from 'ember-osf-web/institutions/dashboard/-components/chart-kpi-wrapper/component'; + +interface KPIChartWrapperArgs { + data: KpiChartModel; +} + +interface DataModel { + name: string; + total: number; + color: string; +} + +export default class ChartKpi extends Component { + @service intl!: Intl; + + @tracked collapsed = true; + @tracked expandedData = [] as DataModel[]; + + /** + * chartOptions + * + * @description A getter for the chartjs options + * + * @returns a ChartOptions model which is custom to COS + */ + get chartOptions(): ChartOptions { + const options = { + aspectRatio: 1, + legend: { + display: false, + }, + scales: { + xAxes: [{ + display: false, + }], + yAxes: [{ + display: false, + ticks: { min: 0 }, + }], + }, + }; + if (this.args.data.chartType === 'bar') { + options.scales.yAxes[0].display = true; + } + return options; + } + + /** + * getColor + * + * @description Gets a specific color using a modulus + * + * @param index The index to retrieve + * + * @returns the color + */ + private getColor(index: number): string { + const backgroundColors = [ + '#00D1FF', + '#009CEF', + '#0063EF', + '#00568D', + '#004673', + '#00375A', + '#263947', + ]; + + return backgroundColors[index % backgroundColors.length]; + } + + /** + * chartData + * + * @description Transforms the standard chart data into data the charts can display + * + * @returns void + */ + get chartData(): ChartData { + const backgroundColors = [] as string[]; + const data = [] as number[]; + const labels = [] as string[]; + const { taskInstance, chartData } = this.args.data; + + const rawData = taskInstance?.value || chartData || []; + + rawData.forEach((rawChartData: ChartDataModel, $index: number) => { + backgroundColors.push(this.getColor($index)); + + data.push(rawChartData.total); + labels.push(rawChartData.label); + this.expandedData.push({ + name: rawChartData.label, + total: rawChartData.total, + color: this.getColor($index), + }); + }); + return { + labels, + datasets: [{ + data, + fill: false, + backgroundColor: backgroundColors, + }], + }; + } + + @action + public toggleExpandedData() { + this.collapsed = !this.collapsed; + } +} diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/styles.scss b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/styles.scss new file mode 100644 index 00000000000..fb6757a930b --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/styles.scss @@ -0,0 +1,116 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.chart-container { + margin-right: 12px; + margin-bottom: 12px; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + align-items: flex-start; + width: 350px; + min-height: 290px; + height: fit-content; + background-color: $color-bg-white; + + .top-container { + width: 100%; + height: 240px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .ember-chart { + max-width: 220px; + max-height: 220px; + } + } + + .bottom-container { + width: 100%; + min-height: 50px; + height: fit-content; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + + .title-container { + width: 100%; + height: 50px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + .title { + font-size: 14px; + font-weight: normal; + height: 25px; + } + + .button-container { + margin-left: 5px; + height: 25px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + } + } + + .expanded-data-container { + width: 100%; + padding-top: 10px; + display: flex; + justify-content: center; + align-items: flex-start; + + &.collapsed { + display: none; + } + + .data-list { + list-style: none; + margin: 0; + padding: 0.2rem; + width: calc(100% - 0.2rem); + border-top: 2px solid $color-bg-gray; + + .data-container { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + width: 282px; + + .name { + margin: 0 5px; + width: calc(282px - 100px); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .color { + width: 20px; + height: 20px; + } + + .total { + width: 80px; + text-align: right; + } + } + } + } + } + + &.mobile { + margin-right: 0; + margin-bottom: 12px; + } +} + + diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/template.hbs b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/template.hbs new file mode 100644 index 00000000000..d55b0fd48ee --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/chart-kpi/template.hbs @@ -0,0 +1,75 @@ +
    + {{#if @data.taskInstance.isRunning}} + + {{else if @data.taskInstance.isError}} + {{t 'institutions.dashboard.kpi-chart.error'}} + {{else}} +
    +
    + +
    +
    +
    + {{#let (unique-id 'expanded-data') as |expandedDataId|}} +
    +
    {{@data.title}}
    +
    + +
    +
    +
    +
      + {{#each this.expandedData as |data index |}} +
    • +
      +
      +
      + {{data.name}} +
      +
      + {{data.total}} +
      +
    • + {{/each}} +
    +
    + {{/let}} +
    + {{/if}} +
    \ No newline at end of file diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts b/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts new file mode 100644 index 00000000000..268b43ef98e --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/component-test.ts @@ -0,0 +1,333 @@ +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { TestContext } from 'ember-test-helpers'; +import { module, test } from 'qunit'; + +module('Integration | institutions | dashboard | -components | kpi-chart-wrapper', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(function(this: TestContext) { + const model = Object({ + summaryMetrics: { + userCount: 10, + privateProjectCount: 15, + publicProjectCount: 20, + publicRegistrationCount: 100, + embargoedRegistrationCount: 200, + publishedPreprintCount: 1000, + storageByteCount: 2000, + }, + departmentMetrics: [ + { + name: 'Math', + numberOfUsers: 25, + }, + { + name: 'Science', + numberOfUsers: 37, + }, + ], + institution: { + iris: ['bleh'], + }, + }); + + this.set('model', model); + }); + + test('it calculates the Total Users by Department data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="0"]'; + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Total Users by Department'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .hasText('Math'); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('25'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .hasText('Science'); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('37'); + + // Finally there are only 2 expanded data points + assert.dom(`${parentDom} [data-test-expanded-name="2"]`) + .doesNotExist(); + }); + + test('it calculates the Public vs Private Project data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="1"]'; + + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Public vs Private Projects'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .hasText('Public Projects'); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('20'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .hasText('Private Projects'); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('15'); + + // Finally there are only 2 expanded data points + assert.dom(`${parentDom} [data-test-expanded-name="2"]`) + .doesNotExist(); + }); + + test('it calculates the Public vs Private Registration data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="2"]'; + + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Public vs Private Registrations'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .hasText('Public Registrations'); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('100'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .hasText('Private Registrations'); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('200'); + + // Finally there are only 2 expanded data points + assert.dom(`${parentDom} [data-test-expanded-name="2"]`) + .doesNotExist(); + }); + + test('it calculates the Total Objects data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="3"]'; + + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Total OSF Objects'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .hasText('Preprints'); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('1000'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .hasText('Public Projects'); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('20'); + + // And the expanded data position 2 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="2"]`) + .hasText('Private Projects'); + + // And the expanded data position 2 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="2"]`) + .hasText('15'); + + // And the expanded data position 3 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="3"]`) + .hasText('Public Registrations'); + + // And the expanded data position 3 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="3"]`) + .hasText('100'); + + // And the expanded data position 4 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="4"]`) + .hasText('Private Registrations'); + + // And the expanded data position 4 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="4"]`) + .hasText('200'); + + // Finally there are only 5 expanded data points + assert.dom(`${parentDom} [data-test-expanded-name="5"]`) + .doesNotExist(); + }); + + test('it calculates the Licenses data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="4"]'; + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Top 10 Licenses'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .exists(); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('3'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .exists(); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('2'); + + assert.dom(`${parentDom} [data-test-expanded-total="2"]`) + .doesNotExist(); + }); + + test('it calculates the Addon data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="5"]'; + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Top 10 Add-ons'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .exists(); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('3'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .exists(); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('2'); + + // Finally there are only 2 expanded data points + assert.dom(`${parentDom} [data-test-expanded-name="2"]`) + .doesNotExist(); + }); + + test('it calculates the Storage Regions data correctly', async function(assert) { + // Given the component is rendered + await render(hbs` + +`); + const parentDom = '[data-test-kpi-chart="6"]'; + // When I click the expanded icon + await click(`${parentDom} [data-test-expand-additional-data]`); + + // And the title is verified + assert.dom(`${parentDom} [data-test-chart-title]`) + .hasText('Top Storage Regions'); + + // And the expanded data position 0 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="0"]`) + .exists(); + + // And the expanded data position 0 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="0"]`) + .hasText('3'); + + // And the expanded data position 1 name is verified + assert.dom(`${parentDom} [data-test-expanded-name="1"]`) + .exists(); + + // And the expanded data position 1 total is verified + assert.dom(`${parentDom} [data-test-expanded-total="1"]`) + .hasText('2'); + + // Finally there are only 2 expanded data points + assert.dom(`${parentDom} [data-test-expanded-name="2"]`) + .doesNotExist(); + }); + + test('it renders the dashboard total charts correctly', async assert => { + // Given the component is rendered + await render(hbs` + +`); + + // Then there are only 8 charts + assert.dom('[data-test-kpi-chart="8"]') + .doesNotExist('There are only 8 charts'); + }); +}); diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/component.ts b/app/institutions/dashboard/-components/chart-kpi-wrapper/component.ts new file mode 100644 index 00000000000..4bc74e2cdbe --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/component.ts @@ -0,0 +1,224 @@ +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task, TaskInstance } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import Intl from 'ember-intl/services/intl'; + +import InstitutionDepartmentModel from 'ember-osf-web/models/institution-department'; +import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric'; +import SearchResultModel from 'ember-osf-web/models/search-result'; + +/* +import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric'; +*/ + +export interface ChartDataModel { + label: string; + total: number; +} + +interface TotalCountChartWrapperArgs { + model: any; +} + +export interface KpiChartModel { + title: string; + chartType: string; + // Either chartData or taskInstance should be defined + chartData?: ChartDataModel[]; + taskInstance?: TaskInstance; +} + +export default class ChartKpiWrapperComponent extends Component { + @service intl!: Intl; + @service store!: Store; + + @tracked model = this.args.model; + @tracked kpiCharts = [] as KpiChartModel[]; + @tracked isLoading = true; + + constructor(owner: unknown, args: TotalCountChartWrapperArgs) { + super(owner, args); + + taskFor(this.loadData).perform(); + } + + /** + * loadData + * + * @description Loads all the data and builds the chart data before rendering the page + * + * @returns a void Promise + */ + @task + @waitFor + private async loadData(): Promise { + const metrics = await this.model; + + const getLicenseTask = taskFor(this.getShareData).perform('rights'); + const getAddonsTask = taskFor(this.getShareData).perform('hasOsfAddon'); + const getRegionTask = taskFor(this.getShareData) + .perform('storageRegion'); + + this.kpiCharts.push( + { + title: this.intl.t('institutions.dashboard.kpi-chart.users-by-department'), + chartData: this.calculateUsersByDepartment(metrics.departmentMetrics), + chartType: 'doughnut', + }, + { + title: this.intl.t('institutions.dashboard.kpi-chart.public-vs-private-projects.title'), + chartData: this.calculateProjects(metrics.summaryMetrics), + chartType: 'doughnut', + }, + { + title: this.intl.t('institutions.dashboard.kpi-chart.public-vs-private-registrations.title'), + chartData: this.calculateRegistrations(metrics.summaryMetrics), + chartType: 'doughnut', + }, + { + title: this.intl.t('institutions.dashboard.kpi-chart.total-osf-objects.title'), + chartData: this.calculateOSFObjects(metrics.summaryMetrics), + chartType: 'doughnut', + }, + { + title: this.intl.t('institutions.dashboard.kpi-chart.licenses'), + chartType: 'bar', + taskInstance: getLicenseTask, + }, + { + title: this.intl.t('institutions.dashboard.kpi-chart.add-ons'), + chartType: 'bar', + taskInstance: getAddonsTask, + }, + { + title: this.intl.t('institutions.dashboard.kpi-chart.storage-regions'), + chartType: 'doughnut', + taskInstance: getRegionTask, + }, + ); + + this.isLoading = false; + } + + /** + * calculateUserByDepartments + * + * @description Abstracted method to build the ChartData model for departments + * @param departmentMetrics The department metrics object + * + * @returns The users by department ChartData model + */ + private calculateUsersByDepartment(departmentMetrics: InstitutionDepartmentModel[]): ChartDataModel[] { + const departmentData = [] as ChartDataModel[]; + + departmentMetrics.forEach((metric: InstitutionDepartmentModel ) => { + departmentData.push( + { + label: metric.name, + total: metric.numberOfUsers, + } as ChartDataModel, + ); + }); + return departmentData; + } + + /** + * calculateRegistrations + * + * @description Abstracted method to calculate the private and public registrations + * @param summaryMetrics The institutional summary metrics object + * + * @returns The total of private and public registrations + */ + private calculateRegistrations(summaryMetrics: InstitutionSummaryMetricModel): ChartDataModel[] { + return [ + { + label: this.intl.t('institutions.dashboard.kpi-chart.public-vs-private-registrations.public'), + total: summaryMetrics.publicRegistrationCount, + } as ChartDataModel, + { + label: this.intl.t('institutions.dashboard.kpi-chart.public-vs-private-registrations.private'), + total: summaryMetrics.embargoedRegistrationCount, + } as ChartDataModel, + ]; + } + + /** + * calculateOSFObjects + * + * @description Abstracted method to calculate the osf objects + * + * @param summaryMetrics The institutional summary metrics object + * + * @returns The total OSF objects + */ + private calculateOSFObjects(summaryMetrics: InstitutionSummaryMetricModel): ChartDataModel[] { + let chartData = [ + { + label: this.intl.t('institutions.dashboard.kpi-chart.total-osf-objects.preprints'), + total: summaryMetrics.publishedPreprintCount, + } as ChartDataModel, + ]; + + chartData = chartData.concat(this.calculateProjects(summaryMetrics)); + + chartData = chartData.concat(this.calculateRegistrations(summaryMetrics)); + + return chartData; + } + + /** + * calculateProjects + * + * @description Abstracted method to calculate the private and public projects + * @param summaryMetrics The institutional summary metrics object + * + * @returns The total of private and public projects + */ + private calculateProjects(summaryMetrics: InstitutionSummaryMetricModel): ChartDataModel[] { + return [ + { + label: this.intl.t('institutions.dashboard.kpi-chart.public-vs-private-projects.public'), + total: summaryMetrics.publicProjectCount, + } as ChartDataModel, + { + label: this.intl.t('institutions.dashboard.kpi-chart.public-vs-private-projects.private'), + total: summaryMetrics.privateProjectCount, + } as ChartDataModel, + ]; + } + + /** + * getShareData + * + * @description Abstracted task to fetch data associated with the institution from SHARE + * @param propertyPath The property path to search for + * (e.g. propertyPathKey in the `related-property-path` of an index-card-search) + * + * @returns ChartDataModel[] The labels and totals for each section + * + */ + @task + @waitFor + private async getShareData( + propertyPath: string, + ): Promise { + const valueSearch = await this.store.queryRecord('index-value-search', { + cardSearchFilter: { + affiliation: this.args.model.institution.iris.join(','), + }, + 'page[size]': 10, + valueSearchPropertyPath: propertyPath, + }); + const resultPage = valueSearch.searchResultPage.toArray(); + + return resultPage.map((result: SearchResultModel) => ({ + total: result.cardSearchResultCount, + label: result.indexCard.get('label'), + })); + } +} diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/styles.scss b/app/institutions/dashboard/-components/chart-kpi-wrapper/styles.scss new file mode 100644 index 00000000000..240c5812664 --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/styles.scss @@ -0,0 +1,29 @@ +.wrapper-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + width: calc(100% - 24px); + min-height: 290px; + height: fit-content; + margin-left: 12px; + margin-right: 12px; + margin-bottom: 12px; + + .loading { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + height: 290px; + } + + &.mobile { + flex-direction: column; + height: fit-content; + align-items: center; + margin-bottom: 0; + } +} diff --git a/app/institutions/dashboard/-components/chart-kpi-wrapper/template.hbs b/app/institutions/dashboard/-components/chart-kpi-wrapper/template.hbs new file mode 100644 index 00000000000..7dad4cbed7d --- /dev/null +++ b/app/institutions/dashboard/-components/chart-kpi-wrapper/template.hbs @@ -0,0 +1,14 @@ +
    + {{#if this.isLoading}} +
    + +
    + {{else}} + {{#each this.kpiCharts as |kpiChart index|}} + + {{/each}} + {{/if}} +
    \ No newline at end of file diff --git a/app/institutions/dashboard/-components/departments-panel/component.ts b/app/institutions/dashboard/-components/departments-panel/component.ts deleted file mode 100644 index b40dd29b687..00000000000 --- a/app/institutions/dashboard/-components/departments-panel/component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import Component from '@ember/component'; -import { action, computed } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { ChartData, ChartOptions, Shape } from 'ember-cli-chart'; -import Intl from 'ember-intl/services/intl'; -import { Department } from 'ember-osf-web/models/institution'; -import InstitutionDepartmentsModel from 'ember-osf-web/models/institution-department'; - -export default class DepartmentsPanel extends Component { - @service intl!: Intl; - - topDepartments!: InstitutionDepartmentsModel[]; - totalUsers!: number; - - chartHoverIndex = 0; - - get chartOptions(): ChartOptions { - return { - aspectRatio: 1, - legend: { - display: false, - }, - onHover: this.onChartHover, - }; - } - - @action - onChartHover(_: MouseEvent, shapes: Shape[]) { - if (shapes.length === 0 || this.chartHoverIndex === shapes[0]._index) { - return; - } - this.set('chartHoverIndex', shapes[0]._index); - } - - @computed('topDepartments', 'totalUsers') - get displayDepartments() { - const departments = this.topDepartments.map(({ name, numberOfUsers }) => ({ name, numberOfUsers })); - const departmentNumbers = this.topDepartments.map(x => x.numberOfUsers); - const otherDepartmentNumber = this.totalUsers - departmentNumbers.reduce((a, b) => a + b); - - return [...departments, { name: this.intl.t('general.other'), numberOfUsers: otherDepartmentNumber }]; - } - - @computed('chartHoverIndex', 'displayDepartments.[]') - get chartData(): ChartData { - const backgroundColors = this.displayDepartments.map((_, i) => { - if (i === this.chartHoverIndex) { - return '#15a5eb'; - } - return '#a5b3bd'; - }); - const displayDepartmentNames = this.displayDepartments.map(({ name }) => name); - const displayDepartmentNumbers = this.displayDepartments.map(({ numberOfUsers }) => numberOfUsers); - - return { - labels: displayDepartmentNames, - datasets: [{ - data: displayDepartmentNumbers, - backgroundColor: backgroundColors, - }], - }; - } - - @computed('chartHoverIndex', 'displayDepartments.[]') - get activeDepartment(): Department { - return this.displayDepartments[this.chartHoverIndex]; - } - - @computed('activeDepartment.numberOfUsers', 'displayDepartments') - get activeDepartmentPercentage(): string { - const numUsersArray = this.displayDepartments.map(({ numberOfUsers }) => numberOfUsers); - const count = numUsersArray.reduce((a, b) => a + b); - return ((this.activeDepartment.numberOfUsers / count) * 100).toFixed(2); - } -} diff --git a/app/institutions/dashboard/-components/departments-panel/styles.scss b/app/institutions/dashboard/-components/departments-panel/styles.scss deleted file mode 100644 index 6d16df12aa5..00000000000 --- a/app/institutions/dashboard/-components/departments-panel/styles.scss +++ /dev/null @@ -1,16 +0,0 @@ -.ember-chart { - max-width: 200px; - max-height: 200px; - margin: 0 auto 15px; -} - -.department { - font-size: 16px; - - h3 { - margin: 0 0 10px; - font-size: 24px; - font-weight: bold; - } -} - diff --git a/app/institutions/dashboard/-components/departments-panel/template.hbs b/app/institutions/dashboard/-components/departments-panel/template.hbs deleted file mode 100644 index b07bd993c49..00000000000 --- a/app/institutions/dashboard/-components/departments-panel/template.hbs +++ /dev/null @@ -1,20 +0,0 @@ -{{#if this.topDepartments}} -
    - -
    -
    -

    {{this.activeDepartment.name}}

    -

    - {{this.activeDepartmentPercentage}}%: {{this.activeDepartment.numberOfUsers}} {{t 'institutions.dashboard.users'}} -

    -
    -{{else}} - {{t 'institutions.dashboard.empty'}} -{{/if}} \ No newline at end of file diff --git a/app/institutions/dashboard/-components/institutional-dashboard-wrapper/styles.scss b/app/institutions/dashboard/-components/institutional-dashboard-wrapper/styles.scss new file mode 100644 index 00000000000..4e024d79963 --- /dev/null +++ b/app/institutions/dashboard/-components/institutional-dashboard-wrapper/styles.scss @@ -0,0 +1,54 @@ +@import 'app/styles/layout'; +@import 'app/styles/components'; + +.container { + > div { // override OsfLayout styles for forcing drawer mode + overflow-x: hidden; + } +} + +.heading-wrapper { + border-bottom: 1px solid #ddd; +} + +.banner { + @include clamp-width; + padding: 15px 0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.institution-banner { + max-width: 100%; + max-height: 300px; +} + +.tab-list { + @include clamp-width; + @include tab-list; + white-space: nowrap; + display: flex; + flex-wrap: nowrap; + position: relative; + overflow-x: auto; + margin-bottom: 0; + border-bottom: 0; + + li { + display: inline-flex; + padding: 5px 10px; + + &:has(a:global(.active)) { + background-color: $bg-light; + border-bottom: 2px solid $color-blue; + } + + &:has(a:hover) { + border-color: transparent; + text-decoration: none; + background-color: $bg-light; + color: var(--primary-color); + } + } +} diff --git a/app/institutions/dashboard/-components/institutional-dashboard-wrapper/template.hbs b/app/institutions/dashboard/-components/institutional-dashboard-wrapper/template.hbs new file mode 100644 index 00000000000..1815421af6d --- /dev/null +++ b/app/institutions/dashboard/-components/institutional-dashboard-wrapper/template.hbs @@ -0,0 +1,85 @@ + + +
    + {{@institution.name}} +
    +
      +
    • + + {{t 'institutions.dashboard.tabs.summary'}} + +
    • +
    • + + {{t 'institutions.dashboard.tabs.users'}} + +
    • +
    • + + {{t 'institutions.dashboard.tabs.projects'}} + +
    • +
    • + + {{t 'institutions.dashboard.tabs.registrations'}} + +
    • +
    • + + {{t 'institutions.dashboard.tabs.preprints'}} + +
    • +
    +
    + + {{yield (hash + left=layout.left + right=layout.right + top=layout.top + main=layout.main + )}} +
    diff --git a/app/institutions/dashboard/-components/institutional-users-list/component.ts b/app/institutions/dashboard/-components/institutional-users-list/component.ts index 992d612306f..62c32591dc6 100644 --- a/app/institutions/dashboard/-components/institutional-users-list/component.ts +++ b/app/institutions/dashboard/-components/institutional-users-list/component.ts @@ -1,60 +1,183 @@ -import Component from '@ember/component'; -import { action, computed } from '@ember/object'; -import { reads } from '@ember/object/computed'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; -import { restartableTask, TaskInstance, timeout } from 'ember-concurrency'; +import { restartableTask, timeout } from 'ember-concurrency'; import Intl from 'ember-intl/services/intl'; -import { InstitutionsDashboardModel } from 'ember-osf-web/institutions/dashboard/route'; import InstitutionModel from 'ember-osf-web/models/institution'; import InstitutionDepartmentsModel from 'ember-osf-web/models/institution-department'; import Analytics from 'ember-osf-web/services/analytics'; -export default class InstitutionalUsersList extends Component { +interface Column { + key: string; + selected: boolean; + label: string; + sort_key: string | false; + type: 'string' | 'date_by_month' | 'osf_link' | 'user_name' | 'orcid'; +} + +interface InstitutionalUsersListArgs { + institution: InstitutionModel; + departmentMetrics: InstitutionDepartmentsModel[]; +} + +export default class InstitutionalUsersList extends Component { @service analytics!: Analytics; @service intl!: Intl; - @reads('modelTaskInstance.value.institution') institution?: InstitutionModel; - @reads('modelTaskInstance.value.departmentMetrics') departmentMetrics?: InstitutionDepartmentsModel[]; + // Properties + @tracked department = this.defaultDepartment; + @tracked sort = 'user_name'; + @tracked selectedDepartments: string[] = []; + @tracked filteredUsers = []; + + @tracked columns: Column[] = [ + { + key: 'user_name', + sort_key: 'user_name', + label: this.intl.t('institutions.dashboard.users_list.name'), + selected: true, + type: 'user_name', + }, + { + key: 'department', + sort_key: 'department', + label: this.intl.t('institutions.dashboard.users_list.department'), + selected: true, + type: 'string', + }, + { + key: 'osf_link', + sort_key: false, + label: this.intl.t('institutions.dashboard.users_list.osf_link'), + selected: true, + type: 'osf_link', + }, + { + key: 'orcid', + sort_key: false, + label: this.intl.t('institutions.dashboard.users_list.orcid'), + selected: true, + type: 'orcid', + }, + { + key: 'publicProjects', + sort_key: 'public_projects', + label: this.intl.t('institutions.dashboard.users_list.public_projects'), + selected: true, + type: 'string', + }, + { + key: 'privateProjects', + sort_key: 'private_projects', + label: this.intl.t('institutions.dashboard.users_list.private_projects'), + selected: true, + type: 'string', + }, + { + key: 'publicRegistrationCount', + sort_key: 'public_registration_count', + label: this.intl.t('institutions.dashboard.users_list.public_registration_count'), + selected: true, + type: 'string', + }, + { + key: 'embargoedRegistrationCount', + sort_key: 'embargoed_registration_count', + label: this.intl.t('institutions.dashboard.users_list.private_registration_count'), + selected: true, + type: 'string', + }, + { + key: 'publishedPreprintCount', + sort_key: 'published_preprint_count', + label: this.intl.t('institutions.dashboard.users_list.published_preprint_count'), + selected: true, + type: 'string', + }, + { + key: 'publicFileCount', + sort_key: 'public_file_count', + label: this.intl.t('institutions.dashboard.users_list.public_file_count'), + selected: false, + type: 'string', + }, + { + key: 'userDataUsage', + sort_key: 'storage_byte_count', + label: this.intl.t('institutions.dashboard.users_list.storage_byte_count'), + selected: false, + type: 'string', + }, + { + key: 'accountCreationDate', + sort_key: 'account_creation_date', + label: this.intl.t('institutions.dashboard.users_list.account_created'), + selected: false, + type: 'date_by_month', + }, + { + key: 'monthLastLogin', + sort_key: 'month_last_login', + label: this.intl.t('institutions.dashboard.users_list.month_last_login'), + selected: false, + type: 'date_by_month', + }, + { + key: 'monthLastActive', + sort_key: 'month_last_active', + label: this.intl.t('institutions.dashboard.users_list.month_last_active'), + selected: false, + type: 'date_by_month', + }, + ]; + + @tracked selectedColumns: string[] = this.columns.filter(col => col.selected).map(col => col.key); // Private properties - modelTaskInstance!: TaskInstance; - department = this.intl.t('institutions.dashboard.select_default'); - sort = 'user_name'; + @tracked hasOrcid = false; + @tracked totalUsers = 0; + orcidUrlPrefix = 'https://orcid.org/'; - reloadUserList?: () => void; + @action + toggleColumnSelection(columnKey: string) { + const column = this.columns.find(col => col.key === columnKey); + if (column) { + column.selected = !column.selected; + } + } - @computed('intl.locale') get defaultDepartment() { return this.intl.t('institutions.dashboard.select_default'); } - @computed('defaultDepartment', 'department', 'departmentMetrics.[]', 'institution') get departments() { let departments = [this.defaultDepartment]; - if (this.institution && this.departmentMetrics) { - const institutionDepartments = this.departmentMetrics.map((x: InstitutionDepartmentsModel) => x.name); + if (this.args.institution && this.args.departmentMetrics) { + const institutionDepartments = this.args.departmentMetrics.map((x: InstitutionDepartmentsModel) => x.name); departments = departments.concat(institutionDepartments); } return departments; } - @computed('defaultDepartment', 'department') get isDefaultDepartment() { return this.department === this.defaultDepartment; } - @computed('department', 'isDefaultDepartment', 'sort') get queryUsers() { const query = {} as Record; if (this.department && !this.isDefaultDepartment) { query['filter[department]'] = this.department; } + if (this.hasOrcid) { + query['filter[orcid_id][ne]'] = ''; + } if (this.sort) { query.sort = this.sort; } @@ -66,7 +189,7 @@ export default class InstitutionalUsersList extends Component { async searchDepartment(name: string) { await timeout(500); if (this.institution) { - const depts: InstitutionDepartmentsModel[] = await this.institution.queryHasMany('departmentMetrics', { + const depts: InstitutionDepartmentsModel[] = await this.args.institution.queryHasMany('departmentMetrics', { filter: { name, }, @@ -78,19 +201,41 @@ export default class InstitutionalUsersList extends Component { @action onSelectChange(department: string) { - this.analytics.trackFromElement(this.element, { - name: 'Department Select - Change', - category: 'select', - action: 'change', - }); - this.set('department', department); - if (this.reloadUserList) { - this.reloadUserList(); + this.department = department; + } + + @action + sortInstitutionalUsers(sortBy: string) { + if (this.sort === sortBy) { + // If the current sort is ascending, toggle to descending + this.sort = `-${sortBy}`; + } else if (this.sort === `-${sortBy}`) { + // If the current sort is descending, toggle to ascending + this.sort = sortBy; + } else { + // Set to descending if it's a new sort field + this.sort = `-${sortBy}`; } } @action - sortInstitutionalUsers(sort: string) { - this.set('sort', sort); + cancelSelection() { + this.selectedDepartments = []; + } + + @action + applyColumnSelection() { + this.selectedColumns = this.columns.filter(col => col.selected).map(col => col.key); } + + @action + toggleOrcidFilter(hasOrcid: boolean) { + this.hasOrcid = hasOrcid; + } + + @action + clickToggleOrcidFilter(hasOrcid: boolean) { + this.hasOrcid = !hasOrcid; + } + } diff --git a/app/institutions/dashboard/-components/institutional-users-list/styles.scss b/app/institutions/dashboard/-components/institutional-users-list/styles.scss index ba468a48f50..3a2c345a0c8 100644 --- a/app/institutions/dashboard/-components/institutional-users-list/styles.scss +++ b/app/institutions/dashboard/-components/institutional-users-list/styles.scss @@ -1,26 +1,27 @@ .select { max-width: 320px; padding: 7px 16px 7px 14px; - margin-bottom: 15px; border-color: #ddd; border-radius: 2px; - color: #337ab7; + color: $color-select; } .table { margin-bottom: 45px; table { + overflow-x: auto; + display: block; width: 100%; margin-bottom: 15px; - table-layout: fixed; + table-layout: auto; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border-collapse: collapse; } th, td { - padding: 15px; - overflow: hidden; + padding: 10px; text-overflow: ellipsis; white-space: nowrap; } @@ -35,17 +36,8 @@ } .header { - th { - background: #365063; - border: 0; - color: #fff; - text-transform: uppercase; - vertical-align: middle; - } - - .nested-header { - padding: 0 15px; - } + background: #365063; + color: #fff; } .item { @@ -65,20 +57,9 @@ } } -.sort-button { - display: inline; +.sort-arrow { padding-left: 4px; - button, - button:active, - button:focus, - button:focus:active, - button:hover { - padding-top: 0; - height: 1em; - margin-top: -10px; - } - :global(.btn.selected) { color: #fff !important; } @@ -93,6 +74,235 @@ } } -.text-center { +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.header-text { + text-overflow: ellipsis; + flex-grow: 1; +} + +.sort-arrow-container { + display: inline-flex; + align-items: center; + margin-left: 4px; +} + +.sort-arrow { + display: inline-block; + vertical-align: middle; + color: #fff; +} + +.select-container { + width: 100%; + display: flex; + justify-content: flex-end; + float: right; +} + +.select { + min-width: 120px; + padding: 7px 16px 7px 14px; + border-color: $color-border-gray; + border-radius: 2px; + color: $color-select; text-align: center; + margin: 15px; + + span { + margin-left: 0; + } +} + +.filter-container { + display: flex; + justify-content: flex-start; + align-items: center; + width: 100%; +} + +.dropdown-panel { + position: absolute; + top: calc(100% + 5px); + right: 0; + background-color: $color-bg-white; + border: 1px solid $color-border-gray; + border-radius: 4px; + padding: 15px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + width: 220px; + + &.mobile { + max-width: none; + } +} + +.dropdown-content { + display: flex; + flex-direction: column; + margin-bottom: 10px; +} + +.dropdown-trigger { + padding: 9px; + color: $color-select; +} + +.dropdown-content label { + display: flex; + align-items: center; + padding: 4px 0; + font-size: 14px; + font-weight: lighter; +} + +.dropdown-content [type='checkbox'] { + margin-right: 8px; + cursor: pointer; +} + +.dropdown-actions { + display: flex; + justify-content: flex-end; + padding-top: 10px; + border-top: 1px solid $color-light; +} + +.icon-columns { + padding-right: 5px; +} + +.filter-controls { + display: flex; + align-items: center; + gap: 20px; +} + +.orcid-switch { + display: flex; + align-items: center; +} + +.orcid-toggle-label { + margin-right: 10px; + font-size: 14px; + color: #333; + white-space: nowrap; + font-weight: lighter; +} + +.switch { + position: relative; + display: inline-flex; + width: 60px; + height: 30px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 100%; + border-radius: 34px; + transition: background-color 0.4s; + background-color: #ccc; + cursor: pointer; + position: flex; +} + +.slider::before { + content: ''; + height: 24px; + width: 24px; + background-color: $color-bg-gray; + margin-left: 3px; + border-radius: 50%; + transition: transform 0.4s, background-color 0.4s; + position: flex; +} + +/* Change handle color when checked */ +input:checked + .slider::before { + background-color: $color-green; +} + +/* Hover effects for handle */ +input:not(:checked) + .slider:hover::before { + background-color: $color-bg-gray-darker; +} + +input:checked + .slider:hover::before { + background-color: $color-green-light; +} + +input:checked + .slider::before { + transform: translateX(30px); +} + +.slider.round { + border-radius: 34px; +} + +.slider.round::before { + border-radius: 50%; +} + +.total-users { + margin-right: auto; /* Aligns text to the left */ + display: block ruby; + align-items: center; +} + +.total-users label { + font-size: 18px; + margin-bottom: 0; + font-weight: normal; +} + +.total-users-count { + font-size: 24px; + margin-right: 5px; + font-weight: bold; +} + +.download-button-group { + display: inline-flex; + padding-left: 10px; + align-content: center; + + div { + color: #2d6a9f; + } + + .download-dropdown { + margin-left: 3px; + } +} + +.flex { + display: flex; + align-items: center; +} + +.top-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; +} + +.right-button-group { + justify-content: flex-end; + margin-right: 15px; } diff --git a/app/institutions/dashboard/-components/institutional-users-list/template.hbs b/app/institutions/dashboard/-components/institutional-users-list/template.hbs index 26aa1a76a95..c7d7e1d4989 100644 --- a/app/institutions/dashboard/-components/institutional-users-list/template.hbs +++ b/app/institutions/dashboard/-components/institutional-users-list/template.hbs @@ -1,80 +1,188 @@ {{#if this.modelTaskInstance.isRunning}} {{else}} - - {{department}} - - - - - {{#let (component 'sort-button' - class=(local-class 'sort-button') - sortAction=(action this.sortInstitutionalUsers) - sort=this.sort - ) as |SortButton|}} - - - {{t 'institutions.dashboard.users_list.name'}} - - - - {{t 'institutions.dashboard.users_list.department'}} - - - - {{t 'institutions.dashboard.users_list.projects'}} - - - - - {{t - - - {{t - - - {{/let}} - - - {{#if institutionalUser}} - - - {{institutionalUser.userName}} ({{institutionalUser.userGuid}}) +
    +
    + + {{this.totalUsers}} + + {{t 'institutions.dashboard.users_list.total_users'}} +
    +
    +
    +
    + + +
    + + {{department}} + +
    +
    + + + {{#if dd.isOpen}} +
    +
    + {{#each this.columns as |column|}} + + {{/each}} +
    +
    + + +
    +
    + {{/if}} +
    +
    + {{#if @institution.linkToExternalReportsArchive}} + + + {{t 'institutions.dashboard.download_past_reports_label'}} + + + {{/if}} +
    + + + + + + + + + + + +
    +
    +
    +
    +
    + + + {{#let (component 'sort-arrow' + class=(local-class 'sort-arrow') + sortAction=this.sortInstitutionalUsers + sort=this.sort + ) as |SortArrow|}} + + {{#each this.columns as |column|}} + {{#if (includes column.key this.selectedColumns)}} + +
    + {{column.label}} + {{#if column.sort_key}} + + + + {{/if}} +
    + + {{/if}} + {{/each}} + + {{/let}} +
    + + {{#each this.columns as |column|}} + {{#if (includes column.key this.selectedColumns)}} + + {{#if (eq column.type 'user_name')}} + + {{institutionalUser.userName}} + + {{else if (eq column.type 'osf_link')}} + + {{institutionalUser.userGuid}} + + {{else if (eq column.type 'orcid')}} + {{#if institutionalUser.orcidId}} + + {{institutionalUser.orcidId}} + + {{else}} + {{t 'institutions.dashboard.object-list.table-items.missing-info'}} + {{/if}} + {{else if (eq column.type 'date_by_month')}} + {{#if (get institutionalUser column.key)}} + {{moment-format (get institutionalUser column.key) 'MM/YYYY'}} + {{else}} + {{t 'institutions.dashboard.users_list.not_found'}} + {{/if}} + {{else}} + {{get institutionalUser column.key}} + {{/if}} - {{institutionalUser.department}} - {{institutionalUser.publicProjects}} - {{institutionalUser.privateProjects}} - {{else}} - {{placeholder.text lines=1}} - {{placeholder.text lines=1}} - {{placeholder.text lines=1}} - {{placeholder.text lines=1}} {{/if}} - - - {{t 'institutions.dashboard.users_list.empty'}} - -
    -
    + {{/each}} + + + {{t 'institutions.dashboard.users_list.empty'}} + + {{/if}} diff --git a/app/institutions/dashboard/-components/object-list/component-test.ts b/app/institutions/dashboard/-components/object-list/component-test.ts new file mode 100644 index 00000000000..4c1ae753cb3 --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/component-test.ts @@ -0,0 +1,168 @@ +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { TestContext } from 'ember-test-helpers'; +import { module, test } from 'qunit'; + +import { OsfLinkRouterStub } from 'ember-osf-web/tests/integration/helpers/osf-link-router-stub'; + +module('Integration | institutions | dashboard | -components | object-list', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(function(this: TestContext) { + this.owner.unregister('service:router'); + this.owner.register('service:router', OsfLinkRouterStub); + const columns = [ + { + name: 'Title', + sortKey: 'title', + getValue: () => 'Title of some object', + }, + { + name: 'Description', + getValue: () => 'Description of some object', + }, + { + name: 'Contributors', + type: 'contributors', + }, + { + name: 'DOI', + type: 'doi', + }, + ]; + const institution = server.create('institution', { + id: 'my-institution', + }); + const defaultQueryOptions = { + cardSearchFilter: { + resourceType: 'Project,ProjectComponent', + }, + }; + this.setProperties({ + columns, + institution, + defaultQueryOptions, + objectType: 'thingies', + }); + }); + + test('the table headers are correct', async function(assert) { + await render(hbs` + + `); + + // Elements from InstitutionDashboarWrapper are present + assert.dom('[data-test-page-tab="summary"]').exists('Summary tab exists'); + + // Elements in the top bar are present + assert.dom('[data-test-object-count]').containsText('10 total thingies', 'Object count is correct'); + assert.dom('[data-test-toggle-filter-button]').exists('Filter button exists'); + assert.dom('[data-test-customize-columns-button]').exists('Columns button exists'); + + assert.dom('[data-test-object-list-table]').exists('Object list exists'); + + // The table headers are correct + assert.dom('[data-test-column-header]').exists({ count: 4 }, 'There are 4 columns'); + assert.dom('[data-test-column-header="Title"]').containsText('Title'); + assert.dom('[data-test-column-header="Title"] [data-test-sort="title"]').exists('Title is sortable'); + assert.dom('[data-test-column-header="Description"]').containsText('Description'); + assert.dom('[data-test-column-header="Description"] [data-test-sort="description"]') + .doesNotExist('Description is not sortable'); + + // The table data is not blatantly incorrect + assert.dom('[data-test-object-table-body-row]').exists({ count: 10 }, 'There are 10 rows'); + }); + + test('the table supports filtering', async function(assert) { + await render(hbs` + + `); + + await click('[data-test-toggle-filter-button]'); + + assert.dom('[data-test-filter-facet-toggle]').exists({ count: 3 }, '3 filters available'); + + // Open the filter facet and load the values and select the first filter value + await click('[data-test-filter-facet-toggle]'); + await click('[data-test-filter-facet-value] button'); + + assert.dom('[data-test-active-filter]').exists({ count: 1 }, '1 filter active'); + assert.dom('[data-test-remove-active-filter]').exists('Remove filter button exists'); + + await click('[data-test-remove-active-filter]'); + assert.dom('[data-test-active-filter]').doesNotExist('Filter removed'); + }); + + test('the table supports customizing columns', async function(assert) { + await render(hbs` + + `); + + assert.dom('[data-test-column-header]').exists({ count: 4 }, '4 columns available'); + const titleColumn = document.querySelector('[data-test-column-header="Title"]'); + assert.ok(titleColumn, 'Title column is visible'); + + // Open the column customization menu + await click('[data-test-customize-columns-button]'); + assert.dom('[data-test-column-toggle-input]').exists({ count: 4 }, '4 columns available to show/hide'); + assert.dom('[data-test-column-toggle-input="Title"]').isChecked('Title column checkbox is checked'); + assert.dom('[data-test-column-toggle-input="Description"]').isChecked('Description column checkbox is checked'); + + // Toggle off the first column + await click('[data-test-column-toggle-input="Title"]'); + assert.ok(titleColumn, 'Title column still visible after toggling off'); + + // Save changes + await click('[data-test-save-columns-button]'); + assert.dom('[data-test-column-toggle-input]').doesNotExist('Column toggle menu hidden'); + assert.dom('[data-test-column-header="Title"]').doesNotExist('Title column removed'); + assert.dom('[data-test-column-header]').exists({ count: 3 }, '3 columns available'); + + // Open the menu again + await click('[data-test-customize-columns-button]'); + assert.dom('[data-test-column-toggle-input]').exists({ count: 4 }, 'Column toggle menu reopened'); + assert.dom('[data-test-column-toggle-input="Title"]').isNotChecked('Title column checkbox is not checked'); + assert.dom('[data-test-column-toggle-input="Description"]') + .isChecked('Description column checkbox is still checked'); + + // Toggle off all columns, but reset + await click('[data-test-column-toggle-input="Description"]'); + await click('[data-test-column-toggle-input="Contributors"]'); + await click('[data-test-column-toggle-input="DOI"]'); + await click('[data-test-reset-columns-button]'); + assert.dom('[data-test-column-toggle-input]').doesNotExist('Column toggle menu hidden'); + assert.dom('[data-test-column-header]').exists({ count: 3 }, '3 columns available, as we did not save changes'); + + // Open the menu again + await click('[data-test-customize-columns-button]'); + assert.dom('[data-test-column-toggle-input]').exists({ count: 4 }, 'Column toggle menu reopened'); + assert.dom('[data-test-column-toggle-input="Title"]').isNotChecked('Title column checkbox is not checked'); + assert.dom('[data-test-column-toggle-input="Description"]') + .isChecked('Description column checkbox is still checked'); + + // Toggle title back on + await click('[data-test-column-toggle-input="Title"]'); + await click('[data-test-save-columns-button]'); + assert.ok(titleColumn, 'Title column visible again'); + }); +}); diff --git a/app/institutions/dashboard/-components/object-list/component.ts b/app/institutions/dashboard/-components/object-list/component.ts new file mode 100644 index 00000000000..0333bfb59ad --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/component.ts @@ -0,0 +1,120 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import InstitutionModel from 'ember-osf-web/models/institution'; +import { SuggestedFilterOperators } from 'ember-osf-web/models/related-property-path'; +import SearchResultModel from 'ember-osf-web/models/search-result'; +import { Filter } from 'osf-components/components/search-page/component'; + +interface Column { + name: string; + sortKey?: string; + sortParam?: string; +} +interface ValueColumn extends Column { + getValue(searchResult: SearchResultModel): string; +} + +interface LinkColumn extends Column { + getHref(searchResult: SearchResultModel): string; + getLinkText(searchResult: SearchResultModel): string; + type: 'link'; +} + +interface ComponentColumn extends Column { + type: 'doi' | 'contributors'; +} + +export type ObjectListColumn = ValueColumn | LinkColumn | ComponentColumn; + +interface InstitutionalObjectListArgs { + institution: InstitutionModel; + defaultQueryOptions: Record<'cardSearchFilter', Record>; + columns: ObjectListColumn[]; + objectType: string; +} + +export default class InstitutionalObjectList extends Component { + @tracked activeFilters: Filter[] = []; + @tracked page = ''; + @tracked sort = '-dateModified'; + @tracked sortParam?: string; + @tracked visibleColumns = this.args.columns.map(column => column.name); + @tracked dirtyVisibleColumns = [...this.visibleColumns]; // track changes to visible columns before they are saved + + get queryOptions() { + const options = { + cardSearchFilter: { + ...this.args.defaultQueryOptions.cardSearchFilter, + }, + 'page[cursor]': this.page, + 'page[size]': 10, + // sort can look like `sort=dateFieldName` or `sort[integer-value]=fieldName` if sortParam is provided + sort: this.sortParam ? { [this.sortParam]: this.sort } : this.sort, + }; + const fullQueryOptions = this.activeFilters.reduce((acc, filter: Filter) => { + if (filter.suggestedFilterOperator === SuggestedFilterOperators.IsPresent) { + acc.cardSearchFilter[filter.propertyPathKey] = {}; + acc.cardSearchFilter[filter.propertyPathKey][filter.value] = true; + return acc; + } + const currentValue = acc.cardSearchFilter[filter.propertyPathKey]; + acc.cardSearchFilter[filter.propertyPathKey] = + currentValue ? currentValue.concat(filter.value) : [filter.value]; + return acc; + }, options); + return fullQueryOptions; + } + + get valueSearchQueryOptions() { + return { + ...this.queryOptions.cardSearchFilter, + }; + } + + @action + updateVisibleColumns() { + this.visibleColumns = [...this.dirtyVisibleColumns]; + } + + @action + resetColumns() { + this.dirtyVisibleColumns = [...this.visibleColumns]; + } + + @action + toggleColumnVisibility(columnName: string) { + if (this.dirtyVisibleColumns.includes(columnName)) { + this.dirtyVisibleColumns.removeObject(columnName); + } else { + this.dirtyVisibleColumns.pushObject(columnName); + } + } + + @action + toggleFilter(property: Filter) { + this.page = ''; + if (this.activeFilters.includes(property)) { + this.activeFilters.removeObject(property); + } else { + this.activeFilters.pushObject(property); + } + } + + @action + updateSortKey(newSortKey: string, newSortParam?: string) { + this.sortParam = newSortParam; + this.page = ''; + if (this.sort === newSortKey) { + this.sort = '-' + newSortKey; + } else { + this.sort = newSortKey; + } + } + + @action + updatePage(newPage: string) { + this.page = newPage; + } +} diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/component.ts b/app/institutions/dashboard/-components/object-list/contributors-field/component.ts new file mode 100644 index 00000000000..d4923162959 --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/contributors-field/component.ts @@ -0,0 +1,75 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +import InstitutionModel from 'ember-osf-web/models/institution'; +import SearchResultModel from 'ember-osf-web/models/search-result'; +import { AttributionRoleIris } from 'ember-osf-web/models/index-card'; +import { getOsfmapObjects, getSingleOsfmapValue, hasOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld'; + +interface ContributorsFieldArgs { + searchResult: SearchResultModel; + institution: InstitutionModel; +} + +const roleIriToTranslationKey: Record = { + [AttributionRoleIris.Admin]: 'general.permissions.admin', + [AttributionRoleIris.Write]: 'general.permissions.write', + [AttributionRoleIris.Read]: 'general.permissions.read', +}; + + +export default class InstitutionalObjectListContributorsField extends Component { + @service intl!: Intl; + + // Return two contributors affiliated with the institution given with highest permission levels + get topInstitutionAffiliatedContributors() { + const { searchResult, institution } = this.args; + const {resourceMetadata} = searchResult; + const attributions: any[] = getOsfmapObjects(resourceMetadata, ['qualifiedAttribution']); + const contributors = getOsfmapObjects(resourceMetadata, ['creator']); + const institutionIris = institution.iris; + + const affiliatedAttributions = attributions + .filter((attribution: any) => hasInstitutionAffiliation(contributors, attribution, institutionIris)); + const adminAttributions = affiliatedAttributions.filter( + attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Admin), + ); + const writeAttributions = affiliatedAttributions.filter( + attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Write), + ); + const readAttributions = affiliatedAttributions.filter( + attribution => hasOsfmapValue(attribution, ['hadRole'], AttributionRoleIris.Read), + ); + + const prioritizedAttributions = adminAttributions.concat(writeAttributions, readAttributions); + + return prioritizedAttributions.slice(0, 2).map(attribution => { + const contributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent'])); + const roleIri: AttributionRoleIris = getSingleOsfmapValue(attribution, ['hadRole']); + return { + name: getSingleOsfmapValue(contributor,['name']), + url: getSingleOsfmapValue(contributor, ['identifier']), + permissionLevel: this.intl.t(roleIriToTranslationKey[roleIri]), + }; + }); + } +} + +function hasInstitutionAffiliation(contributors: any[], attribution: any, institutionIris: string[]) { + const attributedContributor = getContributorById(contributors, getSingleOsfmapValue(attribution, ['agent'])); + + if (!attributedContributor.affiliation) { + return false; + } + + return attributedContributor.affiliation.some( + (affiliation: any) => affiliation.identifier.some( + (affiliationIdentifier: any) => institutionIris.includes(affiliationIdentifier['@value']), + ), + ); +} + +function getContributorById(contributors: any[], contributorId: string) { + return contributors.find(contributor => contributor['@id'] === contributorId); +} diff --git a/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs b/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs new file mode 100644 index 00000000000..992e6be26a3 --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/contributors-field/template.hbs @@ -0,0 +1,14 @@ +{{#each this.topInstitutionAffiliatedContributors as |contributor|}} +
    + + {{contributor.name}} + + {{t 'institutions.dashboard.object-list.table-items.permission-level' permissionLevel=contributor.permissionLevel}} +
    +{{else}} +
    + {{t 'institutions.dashboard.object-list.table-items.no-contributors'}} +
    +{{/each}} diff --git a/app/institutions/dashboard/-components/object-list/doi-field/component.ts b/app/institutions/dashboard/-components/object-list/doi-field/component.ts new file mode 100644 index 00000000000..5a1e2733b16 --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/doi-field/component.ts @@ -0,0 +1,15 @@ +import Component from '@glimmer/component'; + +import SearchResultModel from 'ember-osf-web/models/search-result'; +import { extractDoi } from 'ember-osf-web/utils/doi'; + +interface DoiFieldArgs { + searchResult: SearchResultModel; +} + +export default class InstitutionalObjectListDoiField extends Component { + get dois() { + const dois = this.args.searchResult.doi; + return dois.map((doi: string) => ({ fullLink: doi, displayText: extractDoi(doi) })); + } +} diff --git a/app/institutions/dashboard/-components/object-list/doi-field/template.hbs b/app/institutions/dashboard/-components/object-list/doi-field/template.hbs new file mode 100644 index 00000000000..59496096bbc --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/doi-field/template.hbs @@ -0,0 +1,5 @@ +{{#each this.dois as |doi|}} + {{doi.displayText}} +{{else}} + {{t 'institutions.dashboard.object-list.table-items.missing-info'}} +{{/each}} diff --git a/app/institutions/dashboard/-components/object-list/styles.scss b/app/institutions/dashboard/-components/object-list/styles.scss new file mode 100644 index 00000000000..5eabae93733 --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/styles.scss @@ -0,0 +1,130 @@ +@import 'app/styles/layout'; + +.top-bar-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1rem 0; +} + +.total-object-count { + align-self: center; + font-size: large; + + .total-object-number { + font-weight: bold; + } +} + +.top-bar-button-wrapper { + display: flex; + + button { + margin-right: 0.5rem; + } +} + +.customize-menu-wrapper { + display: flex; + flex-direction: column; + padding: 0.5rem; + border: 1px solid $color-border-gray; + width: 240px; + + label { + text-wrap: nowrap; + } +} + +.customize-menu-button-wrapper { + display: flex; + justify-content: end; +} + +.table-wrapper { + overflow: auto; +} + +.object-table { + border-collapse: collapse; + + th { + padding: 10px 15px; + + span { + display: flex; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + + td { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding: 10px 15px; + + border: 1px solid $color-border-gray; + } +} + +.object-table-head { + background: $color-bg-gray-blue-dark; + color: $color-text-white; +} + +.bottom-bar-wrapper { + display: flex; + justify-content: end; + margin: 1rem 0; + + button { + margin-left: 0.5rem; + } +} + +.right-wrapper { + min-width: 300px; + padding: 0.5rem; +} + +.right-panel-header { + font-size: 1.5rem; +} + +.close-button { + float: right; +} + +.active-filter-list { + list-style: none; + padding-left: 0; + margin-top: 1rem; + border-top: 1px solid $color-border-gray; + border-bottom: 1px solid $color-border-gray; +} + +.active-filter-item { + display: flex; + margin: 0.5rem 0.2rem; + justify-content: space-between; + + button { + margin-right: -5px; + } +} + +.blue-text-button { + color: $color-link-dark; +} + +.download-dropdown-trigger { + color: $color-bg-blue-dark; +} + +.download-button-group { + align-content: center; + display: inline-flex; + padding-left: 10px; +} diff --git a/app/institutions/dashboard/-components/object-list/template.hbs b/app/institutions/dashboard/-components/object-list/template.hbs new file mode 100644 index 00000000000..2015b283e94 --- /dev/null +++ b/app/institutions/dashboard/-components/object-list/template.hbs @@ -0,0 +1,259 @@ + + + + {{#if list.searchObjectsTask.isRunning}} + + {{else}} +
    + + {{list.totalResultCount}} + {{t 'institutions.dashboard.object-list.total-objects' objectType=@objectType}} + +
    + + + + + {{t 'institutions.dashboard.object-list.customize'}} + + + {{#each @columns as |column|}} + + {{/each}} +
    + + +
    +
    +
    +
    + {{#if @institution.linkToExternalReportsArchive}} +
    + + + {{t 'institutions.dashboard.download_past_reports_label'}} + + + +
    + {{/if}} + + + + + + + + + +
    +
    +
    +
    + + + + {{#let (component 'sort-arrow' + sort=this.sort + ) as |SortArrow| + }} + {{#each @columns as |column|}} + {{#if (includes column.name this.visibleColumns)}} + + {{/if}} + {{/each}} + {{/let}} + + + + + {{#each list.searchResults as |result|}} + + {{#each @columns as |column|}} + {{#if (includes column.name this.visibleColumns)}} + + {{/if}} + {{/each}} + + {{/each}} + +
    + + {{column.name}} + {{#if column.sortKey}} + + {{/if}} + +
    + {{#if (eq column.type 'link')}} + + {{call (fn column.getLinkText result)}} + + {{else if (eq column.type 'doi')}} + + {{else if (eq column.type 'contributors')}} + + {{else}} + {{call (fn column.getValue result)}} + {{/if}} +
    +
    +
    + {{#if list.showFirstPageOption}} + + {{/if}} + {{#if list.hasPrevPage}} + + {{/if}} + {{#if list.hasNextPage}} + + {{/if}} +
    + {{/if}} +
    + {{#if list.relatedProperties}} + + + {{t 'institutions.dashboard.object-list.filter-heading'}} + + + {{#if this.activeFilters}} +
      + {{#each this.activeFilters as |filter|}} +
    • + + {{filter.propertyVisibleLabel}}: + {{filter.label}} + + +
    • + {{/each}} +
    + {{/if}} + + {{#each list.relatedProperties as |property|}} + + {{/each}} + {{#if list.booleanFilters.length}} + + {{/if}} +
    + {{/if}} +
    +
    diff --git a/app/institutions/dashboard/-components/panel/styles.scss b/app/institutions/dashboard/-components/panel/styles.scss deleted file mode 100644 index cba6f52f7da..00000000000 --- a/app/institutions/dashboard/-components/panel/styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -.panel { - .panel-overall { - border: 0; - margin-bottom: 30px; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); - } - - .panel-heading { - background: #365063; - padding: 15px; - border: 0; - } - - .panel-title { - display: block; - float: none; - color: #fff; - font-size: 14px; - font-weight: bold; - text-align: center; - text-transform: uppercase; - line-height: 20px; - } - - .panel-body { - color: #263947; - text-align: center; - - h3 { - font-size: 18pt; - font-weight: 800; - } - } -} diff --git a/app/institutions/dashboard/-components/panel/template.hbs b/app/institutions/dashboard/-components/panel/template.hbs deleted file mode 100644 index d8d1e4fa52f..00000000000 --- a/app/institutions/dashboard/-components/panel/template.hbs +++ /dev/null @@ -1,12 +0,0 @@ - - - {{@title}} - - - {{#if @isLoading}} - - {{else}} -
    {{yield}}
    - {{/if}} -
    -
    \ No newline at end of file diff --git a/app/institutions/dashboard/-components/projects-panel/component.ts b/app/institutions/dashboard/-components/projects-panel/component.ts deleted file mode 100644 index 1e30b36108f..00000000000 --- a/app/institutions/dashboard/-components/projects-panel/component.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Component from '@ember/component'; -import { computed } from '@ember/object'; -import { alias } from '@ember/object/computed'; -import { inject as service } from '@ember/service'; -import { ChartData, ChartOptions } from 'ember-cli-chart'; -import Intl from 'ember-intl/services/intl'; - -import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric'; - -export default class ProjectsPanel extends Component { - summaryMetrics!: InstitutionSummaryMetricModel; - @alias('summaryMetrics.privateProjectCount') numPrivateProjects!: number; - @alias('summaryMetrics.publicProjectCount') numPublicProjects!: number; - @service intl!: Intl; - - chartOptions: ChartOptions = { - aspectRatio: 1, - legend: { - display: false, - }, - }; - - @computed('numPrivateProjects', 'numPublicProjects') - get numProjects(): number { - return this.numPublicProjects + this.numPrivateProjects; - } - - @computed('numPrivateProjects', 'numPublicProjects') - get chartData(): ChartData { - return { - labels: [ - this.intl.t('institutions.dashboard.public'), - this.intl.t('institutions.dashboard.private'), - ], - datasets: [{ - data: [ - this.numPublicProjects, - this.numPrivateProjects, - ], - backgroundColor: [ - '#36b183', - '#a5b3bd', - ], - }], - }; - } -} diff --git a/app/institutions/dashboard/-components/projects-panel/styles.scss b/app/institutions/dashboard/-components/projects-panel/styles.scss deleted file mode 100644 index 51603b6aad1..00000000000 --- a/app/institutions/dashboard/-components/projects-panel/styles.scss +++ /dev/null @@ -1,13 +0,0 @@ -.ember-chart { - max-width: 200px; - max-height: 200px; - margin: 0 auto 15px; -} - -.projects-count { - font-size: 16px; - - span:first-of-type { - margin-right: 15px; - } -} diff --git a/app/institutions/dashboard/-components/projects-panel/template.hbs b/app/institutions/dashboard/-components/projects-panel/template.hbs deleted file mode 100644 index a9a5bc68fd6..00000000000 --- a/app/institutions/dashboard/-components/projects-panel/template.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{#if this.summaryMetrics}} -
    - -
    -

    {{this.numProjects}}

    -
    - - {{this.numPublicProjects}} - {{t 'institutions.dashboard.public'}} - - - {{this.numPrivateProjects}} - {{t 'institutions.dashboard.private'}} - -
    -{{else}} - {{t 'institutions.dashboard.empty'}} -{{/if}} \ No newline at end of file diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts new file mode 100644 index 00000000000..7df5335449e --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component-test.ts @@ -0,0 +1,118 @@ +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { TestContext } from 'ember-test-helpers'; +import { module, test } from 'qunit'; + +module('Integration | institutions | dashboard | -components | total-count-kpi-wrapper', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(function(this: TestContext) { + const model = Object({ + summaryMetrics: { + publicProjectCount: 10, + privateProjectCount: 10, + userCount: 10, + publicRegistrationCount: 100, + publishedPreprintCount: 1000, + embargoedRegistrationCount: 200, + storageByteCount: 104593230, + publicFileCount: 1567, + monthlyLoggedInUserCount: 300, + monthlyActiveUserCount:40, + convertedStorageCount: '104 GB', + }, + }); + + this.set('model', model); + }); + + test('it renders the dashboard total kpis correctly', async assert => { + // Given the component is rendered + await render(hbs` + +`); + + let parentContainer = '[data-test-total-count-kpi="0"]'; + // Then the total users kpi is tested + assert.dom(parentContainer) + .exists('The User Widget exists'); + assert.dom(parentContainer) + .hasText('10 Total Users'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'users'); + + // And the total logged in user kpi is tested + parentContainer = '[data-test-total-count-kpi="1"]'; + assert.dom(parentContainer) + .exists('The Total Monthly Logged in Users Widget exists'); + assert.dom(parentContainer) + .hasText('300 Total Monthly Logged in Users'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'users'); + + // And the total active in user kpi is tested + parentContainer = '[data-test-total-count-kpi="2"]'; + assert.dom(parentContainer) + .exists('The Total Monthly Active Users Widget exists'); + assert.dom(parentContainer) + .hasText('40 Total Monthly Active Users'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'users'); + + // And the total project kpi is tested + parentContainer = '[data-test-total-count-kpi="3"]'; + assert.dom(parentContainer) + .exists('The Project Widget exists'); + assert.dom(parentContainer) + .hasText('20 OSF Public and Private Projects'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'flask'); + + // And the total registration kpi is tested + parentContainer = '[data-test-total-count-kpi="4"]'; + assert.dom(parentContainer) + .exists('The Total Registration Widget exists'); + assert.dom(parentContainer) + .hasText('300 OSF Public and Embargoed Registrations'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'archive'); + + // And the total preprint kpi is tested + parentContainer = '[data-test-total-count-kpi="5"]'; + assert.dom(parentContainer) + .exists('The Total Preprint Widget exists'); + assert.dom(parentContainer) + .hasText('1000 OSF Preprints'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'file-alt'); + + // And the total file count kpi is tested + parentContainer = '[data-test-total-count-kpi="6"]'; + assert.dom(parentContainer) + .exists('The Total File Widget exists'); + assert.dom(parentContainer) + .hasText('1567 Total Public File Count'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'file-alt'); + + // And the total storage kpi is tested + parentContainer = '[data-test-total-count-kpi="7"]'; + assert.dom(parentContainer) + .exists('The Total Storage Widget exists'); + assert.dom(parentContainer) + .hasText('104 Total Storage in GB'); + assert.dom(`${parentContainer} [data-test-kpi-icon]`) + .hasAttribute('data-icon', 'database'); + + // Finally there are only 8 widgets + assert.dom('[data-test-total-count-kpi="8"]') + .doesNotExist('There are only 8 widgets'); + }); +}); diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts new file mode 100644 index 00000000000..ccc9c62e102 --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/component.ts @@ -0,0 +1,101 @@ +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import Intl from 'ember-intl/services/intl'; +import { inject as service } from '@ember/service'; +import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric'; + +interface TotalCountKpiWrapperArgs { + model: any; +} + +export interface TotalCountKpiModel { + title: string; + total: number | string; + icon: string; +} + +export default class TotalCountKpiWrapperComponent extends Component { + @service intl!: Intl; + @tracked model = this.args.model; + @tracked totalCountKpis = [] as TotalCountKpiModel[]; + @tracked isLoading = true; + + constructor(owner: unknown, args: TotalCountKpiWrapperArgs) { + super(owner, args); + + taskFor(this.loadData).perform(); + } + + /** + * calculateProjects + * + * @description Abstracted method to calculate the private and public projects + * @param summaryMetrics The institutional summary metrics object + * + * @returns The total of private and public projects + */ + private calculateProjects(summaryMetrics: InstitutionSummaryMetricModel): number { + return summaryMetrics.privateProjectCount + summaryMetrics.publicProjectCount; + } + + private calculateRegistrations(summaryMetrics: InstitutionSummaryMetricModel): number { + return summaryMetrics.embargoedRegistrationCount + summaryMetrics.publicRegistrationCount; + } + + @task + @waitFor + private async loadData(): Promise { + const metrics: { summaryMetrics: InstitutionSummaryMetricModel } = await this.model; + const [storageAmount, storageUnit] = metrics.summaryMetrics.convertedStorageCount.split(' '); + + this.totalCountKpis.push( + { + title: this.intl.t('institutions.dashboard.kpi-panel.users'), + total: metrics.summaryMetrics.userCount, + icon: 'users', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.logged-in-users'), + total: metrics.summaryMetrics.monthlyLoggedInUserCount, + icon: 'users', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.active-users'), + total: metrics.summaryMetrics.monthlyActiveUserCount, + icon: 'users', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.projects'), + total: this.calculateProjects(metrics.summaryMetrics), + icon: 'flask', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.registrations'), + total: this.calculateRegistrations(metrics.summaryMetrics), + icon: 'archive', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.preprints'), + total: metrics.summaryMetrics.publishedPreprintCount, + icon: 'file-alt', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.file-count'), + total: metrics.summaryMetrics.publicFileCount, + icon: 'file-alt', + }, + { + title: this.intl.t('institutions.dashboard.kpi-panel.storage', { unit: storageUnit }), + total: storageAmount, + icon: 'database', + }, + ); + + this.isLoading = false; + } +} + + diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss b/app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss new file mode 100644 index 00000000000..853bdec6f24 --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/styles.scss @@ -0,0 +1,29 @@ +.wrapper-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + width: calc(100% - 24px); + min-height: 145px; + height: fit-content; + margin-left: 12px; + margin-right: 12px; + margin-bottom: 12px; + + .loading { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + height: 145px; + } + + &.mobile { + flex-direction: column; + height: fit-content; + align-items: center; + margin-bottom: 0; + } +} diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs b/app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs new file mode 100644 index 00000000000..943bca40371 --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/template.hbs @@ -0,0 +1,14 @@ +
    + {{#if this.isLoading}} +
    + +
    + {{else}} + {{#each this.totalCountKpis as |totalCountKpi index|}} + + {{/each}} + {{/if}} +
    \ No newline at end of file diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts new file mode 100644 index 00000000000..69897968789 --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/component-test.ts @@ -0,0 +1,64 @@ +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { EnginesIntlTestContext } from 'ember-engines/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { TestContext } from 'ember-test-helpers'; +import { module, test } from 'qunit'; + +module('Integration | institutions | dashboard | -components | total-count-kpi', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks); + + hooks.beforeEach(function(this: TestContext) { + const data = Object({ + total: 200, + title: 'This is the title', + icon: 'building', + }); + + this.set('data', data); + }); + + test('it renders the data correctly', async assert => { + + await render(hbs` + +`); + + assert.dom('[data-test-kpi-title]') + .hasText('This is the title'); + assert.dom('[data-test-kpi-data]') + .hasText('200'); + assert.dom('[data-test-kpi-icon]') + .hasAttribute('data-icon', 'building'); + }); + + test('it renders the without data correctly', async function(this: EnginesIntlTestContext, assert) { + const data = Object({ + total: 0, + title: 'This is the title', + icon: 'building', + }); + + this.set('data', data); + + + await render(hbs` + +`); + + assert.dom('[data-test-kpi-title]') + .hasText('This is the title'); + assert.dom('[data-test-kpi-data]') + .hasText('No data for institution found.'); + assert.dom('[data-test-kpi-icon]') + .hasAttribute('data-icon', 'building'); + }); +}); diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss new file mode 100644 index 00000000000..ad500d44b1b --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/styles.scss @@ -0,0 +1,67 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.kpi-container { + margin-right: 12px; + margin-bottom: 12px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 350px; + height: 140px; + background-color: $color-bg-white; + + .top-container { + width: 100%; + display: flex; + justify-content: space-between; + padding: 0 15px; + margin-bottom: 10px; + + .left-container { + padding-left: 5px; + height: 75px; + display: flex; + justify-content: flex-start; + align-items: center; + + .total-container { + font-size: 84px; + font-style: normal; + font-weight: bolder; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + + .right-container { + padding-right: 10px; + height: 75px; + width: 75px; + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + + .icon { + font-size: 60px; + color: $color-text-slate-gray; + } + } + + } + + .title { + padding-left: 15px; + width: calc(100% - 15px); + font-size: 14px; + font-weight: normal; + height: 25px; + } + + &.mobile { + margin-right: 0; + margin-bottom: 12px; + } +} diff --git a/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs new file mode 100644 index 00000000000..59484f26916 --- /dev/null +++ b/app/institutions/dashboard/-components/total-count-kpi-wrapper/total-count-kpi/template.hbs @@ -0,0 +1,20 @@ +
    +
    +
    + {{#if @data.total}} +
    + {{@data.total}} +
    + {{else}} + {{t 'institutions.dashboard.empty'}} + {{/if}} +
    +
    + +
    +
    +
    {{@data.title}}
    +
    diff --git a/app/institutions/dashboard/controller.ts b/app/institutions/dashboard/controller.ts deleted file mode 100644 index 785f77100c5..00000000000 --- a/app/institutions/dashboard/controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { alias } from '@ember/object/computed'; -import Controller from '@ember/controller'; -import { computed } from '@ember/object'; -import { inject as service } from '@ember/service'; - -import { InstitutionsDashboardModel } from 'ember-osf-web/institutions/dashboard/route'; -import InstitutionModel from 'ember-osf-web/models/institution'; -import InstitutionDepartmentModel from 'ember-osf-web/models/institution-department'; -import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric'; -import CurrentUser from 'ember-osf-web/services/current-user'; -import { addQueryParam } from 'ember-osf-web/utils/url-parts'; - -export default class InstitutionsDashboardController extends Controller { - @service currentUser!: CurrentUser; - - @alias('model.taskInstance.value') modelValue?: InstitutionsDashboardModel; - @alias('modelValue.institution') institution?: InstitutionModel; - @alias('modelValue.summaryMetrics') summaryMetrics?: InstitutionSummaryMetricModel; - @alias('modelValue.departmentMetrics') departmentMetrics?: InstitutionDepartmentModel[]; - @alias('modelValue.totalUsers') totalUsers?: number; - - @computed('institution') - get csvHref(): string { - const { institution } = this; - if (institution) { - const url = institution.hasMany('userMetrics').link(); - return addQueryParam(url, 'format', 'csv'); - } - return ''; - } -} - -declare module '@ember/controller' { - interface Registry { - 'institutions-dashboard': InstitutionsDashboardController; - } -} diff --git a/app/institutions/dashboard/index/styles.scss b/app/institutions/dashboard/index/styles.scss new file mode 100644 index 00000000000..5df5cd3eeaf --- /dev/null +++ b/app/institutions/dashboard/index/styles.scss @@ -0,0 +1,24 @@ +.main-container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + padding-top: 12px; + background-color: $color-bg-gray; + width: 100%; + + .kpi-container { + margin-bottom: 12px; + } + + .chart-container { + margin-bottom: 20px; + } + + &.mobile { + .kpi-container, + .chart-container { + margin-bottom: 0; + } + } +} diff --git a/app/institutions/dashboard/index/template.hbs b/app/institutions/dashboard/index/template.hbs new file mode 100644 index 00000000000..36145181db9 --- /dev/null +++ b/app/institutions/dashboard/index/template.hbs @@ -0,0 +1,16 @@ + + +
    + +
    +
    + +
    +
    +
    diff --git a/app/institutions/dashboard/preprints/controller.ts b/app/institutions/dashboard/preprints/controller.ts new file mode 100644 index 00000000000..c51f5c42de4 --- /dev/null +++ b/app/institutions/dashboard/preprints/controller.ts @@ -0,0 +1,78 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld'; + +import { ResourceTypeFilterValue } from 'osf-components/components/search-page/component'; +import { ObjectListColumn } from '../-components/object-list/component'; + +export default class InstitutionDashboardPreprints extends Controller { + @service intl!: Intl; + + missingItemPlaceholder = this.intl.t('institutions.dashboard.object-list.table-items.missing-info'); + + columns: ObjectListColumn[] = [ + { // Title + name: this.intl.t('institutions.dashboard.object-list.table-headers.title'), + type: 'link', + getHref: searchResult => searchResult.indexCard.get('osfIdentifier'), + getLinkText: searchResult => searchResult.displayTitle, + }, + { // Link + name: this.intl.t('institutions.dashboard.object-list.table-headers.link'), + type: 'link', + getHref: searchResult => searchResult.indexCard.get('osfIdentifier'), + getLinkText: searchResult => searchResult.indexCard.get('osfGuid'), + }, + { // Date created + name: this.intl.t('institutions.dashboard.object-list.table-headers.created_date'), + getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateCreated']), + sortKey: 'dateCreated', + }, + { // Date modified + name: this.intl.t('institutions.dashboard.object-list.table-headers.modified_date'), + getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateModified']), + sortKey: 'dateModified', + }, + { // DOI + name: this.intl.t('institutions.dashboard.object-list.table-headers.doi'), + type: 'doi', + }, + { // License + name: this.intl.t('institutions.dashboard.object-list.table-headers.license'), + getValue: searchResult => searchResult.license?.name || this.missingItemPlaceholder, + }, + { // Contributor name + permissions + name: this.intl.t('institutions.dashboard.object-list.table-headers.contributor_name'), + type: 'contributors', + }, + { // View count + name: this.intl.t('institutions.dashboard.object-list.table-headers.view_count'), + getValue: searchResult => { + const metrics = searchResult.usageMetrics; + return metrics ? metrics.viewCount : this.missingItemPlaceholder; + }, + sortKey: 'usage.viewCount', + sortParam: 'integer-value', + }, + { // Download count + name: this.intl.t('institutions.dashboard.object-list.table-headers.download_count'), + getValue: searchResult => { + const metrics = searchResult.usageMetrics; + return metrics ? metrics.downloadCount : this.missingItemPlaceholder; + }, + sortKey: 'usage.downloadCount', + sortParam: 'integer-value', + }, + ]; + + get defaultQueryOptions() { + const identifiers = this.model.institution.iris.join(','); + return { + cardSearchFilter: { + affiliation: [identifiers], + resourceType: ResourceTypeFilterValue.Preprints, + }, + }; + } +} diff --git a/app/institutions/dashboard/preprints/route.ts b/app/institutions/dashboard/preprints/route.ts new file mode 100644 index 00000000000..b083dbe9831 --- /dev/null +++ b/app/institutions/dashboard/preprints/route.ts @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class InstitutionsDashboardPreprintsRoute extends Route { +} diff --git a/app/institutions/dashboard/preprints/template.hbs b/app/institutions/dashboard/preprints/template.hbs new file mode 100644 index 00000000000..3a7ffd7838a --- /dev/null +++ b/app/institutions/dashboard/preprints/template.hbs @@ -0,0 +1,6 @@ + diff --git a/app/institutions/dashboard/projects/controller.ts b/app/institutions/dashboard/projects/controller.ts new file mode 100644 index 00000000000..22e27b6d25e --- /dev/null +++ b/app/institutions/dashboard/projects/controller.ts @@ -0,0 +1,100 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld'; + +import humanFileSize from 'ember-osf-web/utils/human-file-size'; +import { ObjectListColumn } from '../-components/object-list/component'; + +export default class InstitutionDashboardProjects extends Controller { + @service intl!: Intl; + + missingItemPlaceholder = this.intl.t('institutions.dashboard.object-list.table-items.missing-info'); + + columns: ObjectListColumn[] = [ + { // Title + name: this.intl.t('institutions.dashboard.object-list.table-headers.title'), + type: 'link', + getHref: searchResult => searchResult.indexCard.get('osfIdentifier'), + getLinkText: searchResult => searchResult.displayTitle, + }, + { // Link + name: this.intl.t('institutions.dashboard.object-list.table-headers.link'), + type: 'link', + getHref: searchResult => searchResult.indexCard.get('osfIdentifier'), + getLinkText: searchResult => searchResult.indexCard.get('osfGuid'), + }, + { // Date created + name: this.intl.t('institutions.dashboard.object-list.table-headers.created_date'), + getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateCreated']), + sortKey: 'dateCreated', + }, + { // Date modified + name: this.intl.t('institutions.dashboard.object-list.table-headers.modified_date'), + getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateModified']), + sortKey: 'dateModified', + }, + { // DOI + name: this.intl.t('institutions.dashboard.object-list.table-headers.doi'), + type: 'doi', + }, + { // Storage location + name: this.intl.t('institutions.dashboard.object-list.table-headers.storage_location'), + getValue: searchResult => searchResult.storageRegion, + }, + { // Total data stored + name: this.intl.t('institutions.dashboard.object-list.table-headers.total_data_stored'), + getValue: searchResult => { + const byteCount = getSingleOsfmapValue(searchResult.resourceMetadata, ['storageByteCount']); + return byteCount ? humanFileSize(byteCount) : this.missingItemPlaceholder; + }, + sortKey: 'storageByteCount', + sortParam: 'integer-value', + }, + { // Contributor name + permissions + name: this.intl.t('institutions.dashboard.object-list.table-headers.contributor_name'), + type: 'contributors', + }, + { // View count + name: this.intl.t('institutions.dashboard.object-list.table-headers.view_count'), + getValue: searchResult => { + const metrics = searchResult.usageMetrics; + return metrics ? metrics.viewCount : this.missingItemPlaceholder; + }, + sortKey: 'usage.viewCount', + sortParam: 'integer-value', + }, + { // Resource type + name: this.intl.t('institutions.dashboard.object-list.table-headers.resource_nature'), + getValue: searchResult => searchResult.resourceNature || this.missingItemPlaceholder, + }, + { // License + name: this.intl.t('institutions.dashboard.object-list.table-headers.license'), + getValue: searchResult => searchResult.license?.name || this.missingItemPlaceholder, + }, + { // addons associated + name: this.intl.t('institutions.dashboard.object-list.table-headers.addons'), + getValue: searchResult => searchResult.configuredAddonNames.length ? searchResult.configuredAddonNames : + this.missingItemPlaceholder, + }, + { // Funder name + name: this.intl.t('institutions.dashboard.object-list.table-headers.funder_name'), + getValue: searchResult => { + if (!searchResult.funders) { + return this.missingItemPlaceholder; + } + return searchResult.funders.map((funder: { name: string }) => funder.name).join(', '); + }, + }, + ]; + + get defaultQueryOptions() { + const identifiers = this.model.institution.iris.join(','); + return { + cardSearchFilter: { + affiliation: [identifiers], + resourceType: 'Project', + }, + }; + } +} diff --git a/app/institutions/dashboard/projects/route.ts b/app/institutions/dashboard/projects/route.ts new file mode 100644 index 00000000000..92028943388 --- /dev/null +++ b/app/institutions/dashboard/projects/route.ts @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class InstitutionsDashboardRoute extends Route { +} diff --git a/app/institutions/dashboard/projects/template.hbs b/app/institutions/dashboard/projects/template.hbs new file mode 100644 index 00000000000..64a49591bea --- /dev/null +++ b/app/institutions/dashboard/projects/template.hbs @@ -0,0 +1,6 @@ + diff --git a/app/institutions/dashboard/registrations/controller.ts b/app/institutions/dashboard/registrations/controller.ts new file mode 100644 index 00000000000..d991172f29f --- /dev/null +++ b/app/institutions/dashboard/registrations/controller.ts @@ -0,0 +1,98 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import { getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld'; + +import humanFileSize from 'ember-osf-web/utils/human-file-size'; +import { ObjectListColumn } from '../-components/object-list/component'; + +export default class InstitutionDashboardRegistrations extends Controller { + @service intl!: Intl; + + missingItemPlaceholder = this.intl.t('institutions.dashboard.object-list.table-items.missing-info'); + columns: ObjectListColumn[] = [ + { // Title + name: this.intl.t('institutions.dashboard.object-list.table-headers.title'), + type: 'link', + getHref: searchResult => searchResult.indexCard.get('osfIdentifier'), + getLinkText: searchResult => searchResult.displayTitle, + }, + { // Link + name: this.intl.t('institutions.dashboard.object-list.table-headers.link'), + type: 'link', + getHref: searchResult => searchResult.indexCard.get('osfIdentifier'), + getLinkText: searchResult => searchResult.indexCard.get('osfGuid'), + }, + { // Date created + name: this.intl.t('institutions.dashboard.object-list.table-headers.created_date'), + getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateCreated']), + sortKey: 'dateCreated', + }, + { // Date modified + name: this.intl.t('institutions.dashboard.object-list.table-headers.modified_date'), + getValue: searchResult => getSingleOsfmapValue(searchResult.resourceMetadata, ['dateModified']), + sortKey: 'dateModified', + }, + { // DOI + name: this.intl.t('institutions.dashboard.object-list.table-headers.doi'), + type: 'doi', + }, + { // Storage location + name: this.intl.t('institutions.dashboard.object-list.table-headers.storage_location'), + getValue: searchResult => searchResult.storageRegion, + }, + { // Total data stored + name: this.intl.t('institutions.dashboard.object-list.table-headers.total_data_stored'), + getValue: searchResult => { + const byteCount = getSingleOsfmapValue(searchResult.resourceMetadata, ['storageByteCount']); + return byteCount ? humanFileSize(byteCount) : this.missingItemPlaceholder; + }, + sortKey: 'storageByteCount', + sortParam: 'integer-value', + }, + { // Contributor name + permissions + name: this.intl.t('institutions.dashboard.object-list.table-headers.contributor_name'), + type: 'contributors', + }, + { // View count + name: this.intl.t('institutions.dashboard.object-list.table-headers.view_count'), + getValue: searchResult => { + const metrics = searchResult.usageMetrics; + return metrics ? metrics.viewCount : this.missingItemPlaceholder; + }, + sortKey: 'usage.viewCount', + sortParam: 'integer-value', + }, + { // Resource type + name: this.intl.t('institutions.dashboard.object-list.table-headers.resource_nature'), + getValue: searchResult => searchResult.resourceNature || this.missingItemPlaceholder, + }, + { // License + name: this.intl.t('institutions.dashboard.object-list.table-headers.license'), + getValue: searchResult => searchResult.license?.name || this.missingItemPlaceholder, + }, + { // Funder name + name: this.intl.t('institutions.dashboard.object-list.table-headers.funder_name'), + getValue: searchResult => { + if (!searchResult.funders) { + return this.missingItemPlaceholder; + } + return searchResult.funders.map((funder: { name: string }) => funder.name).join(', '); + }, + }, + { // schema + name: this.intl.t('institutions.dashboard.object-list.table-headers.registration_schema'), + getValue: searchResult => searchResult.registrationTemplate, + }, + ]; + + get defaultQueryOptions() { + const identifiers = this.model.institution.iris.join(','); + return { + cardSearchFilter: { + affiliation: [identifiers], + resourceType: 'Registration', + }, + }; + } +} diff --git a/app/institutions/dashboard/registrations/route.ts b/app/institutions/dashboard/registrations/route.ts new file mode 100644 index 00000000000..460700e098e --- /dev/null +++ b/app/institutions/dashboard/registrations/route.ts @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class InstitutionsDashboardRegistrationsRoute extends Route { +} diff --git a/app/institutions/dashboard/registrations/template.hbs b/app/institutions/dashboard/registrations/template.hbs new file mode 100644 index 00000000000..afbe7f88a6b --- /dev/null +++ b/app/institutions/dashboard/registrations/template.hbs @@ -0,0 +1,6 @@ + diff --git a/app/institutions/dashboard/route.ts b/app/institutions/dashboard/route.ts index e03dc35352d..16ab7b53501 100644 --- a/app/institutions/dashboard/route.ts +++ b/app/institutions/dashboard/route.ts @@ -1,16 +1,14 @@ import Route from '@ember/routing/route'; import RouterService from '@ember/routing/router-service'; import { inject as service } from '@ember/service'; -import { waitFor } from '@ember/test-waiters'; import Store from '@ember-data/store'; -import { task } from 'ember-concurrency'; -import { taskFor } from 'ember-concurrency-ts'; import InstitutionModel from 'ember-osf-web/models/institution'; import InstitutionDepartmentModel from 'ember-osf-web/models/institution-department'; import InstitutionSummaryMetricModel from 'ember-osf-web/models/institution-summary-metric'; import { QueryHasManyResult } from 'ember-osf-web/models/osf-model'; import captureException from 'ember-osf-web/utils/capture-exception'; +import { notFoundURL } from 'ember-osf-web/utils/clean-url'; export interface InstitutionsDashboardModel { institution: InstitutionModel; @@ -21,16 +19,16 @@ export default class InstitutionsDashboardRoute extends Route { @service router!: RouterService; @service store!: Store; - @task - @waitFor - async modelTask(institutionId: string) { + // eslint-disable-next-line camelcase + async model(params: { institution_id: string }) { try { - const institution = await this.store.findRecord('institution', institutionId, { + const institution = await this.store.findRecord('institution', params.institution_id, { adapterOptions: { include: ['summary_metrics'], }, }); const departmentMetrics = await institution.queryHasMany('departmentMetrics'); + const summaryMetrics = await institution.summaryMetrics; const userMetricInfo: QueryHasManyResult = await institution.queryHasMany( 'userMetrics', @@ -45,15 +43,8 @@ export default class InstitutionsDashboardRoute extends Route { }; } catch (error) { captureException(error); - this.transitionTo('not-found', this.router.get('currentURL').slice(1)); + this.transitionTo('not-found', notFoundURL(window.location.pathname)); return undefined; } } - - // eslint-disable-next-line camelcase - model(params: { institution_id: string }) { - return { - taskInstance: taskFor(this.modelTask).perform(params.institution_id), - }; - } } diff --git a/app/institutions/dashboard/styles.scss b/app/institutions/dashboard/styles.scss deleted file mode 100644 index 3ee33c804b3..00000000000 --- a/app/institutions/dashboard/styles.scss +++ /dev/null @@ -1,100 +0,0 @@ -.banner { - padding: 15px 0; - display: flex; - align-items: center; - justify-content: space-between; - - div { - color: #263947; - } -} - -.dashboard-wrapper { - display: flex; -} - -.table-wrapper { - padding-right: 15px; - flex-grow: 1; -} - -.panel-wrapper { - padding-left: 15px; - text-align: right; -} - -.csv-button { - display: inline-block; - width: 40px; - height: 40px; - background: #fff; - padding: 7px 0; - border: 1px solid #ddd; - margin-bottom: 15px; - text-align: center; - - &:active, - &:hover { - background: #15a5eb; - border-color: #15a5eb; - } -} - -.sso-users-connected { - :global(.panel-body) { - h3 { - margin: 0; - font-size: 6vw; - font-weight: bold; - } - } -} - -.projects { - :global(.panel-body) { - h3 { - margin: 0 0 10px; - font-size: 3.75vw; - font-weight: bold; - } - - p { - margin: 0; - font-size: 16px; - } - } -} - -// Extra large devices (large desktops, 1200px and up) -@media (min-width: 1200px) { - .sso-users-connected { - :global(.panel-body) { - h3 { - font-size: 96px; - } - } - } - - .projects { - :global(.panel-body) { - h3 { - font-size: 72px; - } - } - } -} - -@media (max-width: 767px) { - .dashboard-wrapper { - flex-wrap: wrap-reverse; - } - - .panel-wrapper { - padding-left: 0; - width: 100%; - } - - .table-wrapper { - padding-right: 0; - } -} diff --git a/app/institutions/dashboard/template.hbs b/app/institutions/dashboard/template.hbs index fd5e7b6a496..1980fe88290 100644 --- a/app/institutions/dashboard/template.hbs +++ b/app/institutions/dashboard/template.hbs @@ -1,59 +1,2 @@ -{{page-title (t 'institutions.dashboard.title' institutionName=this.institution.unsafeName)}} -
    -
    - {{this.institution.name}} -
    - {{t 'institutions.dashboard.last_update'}} -
    -
    -
    -
    - -
    -
    - {{#if this.csvHref}} - - - - {{/if}} - - {{#if this.summaryMetrics}} -

    {{this.summaryMetrics.userCount}}

    - {{else}} - {{t 'institutions.dashboard.empty'}} - {{/if}} -
    - - - - - - -
    -
    -
    \ No newline at end of file +{{page-title (t 'institutions.dashboard.title' institutionName=this.model.institution.unsafeName)}} +{{outlet}} diff --git a/app/institutions/dashboard/users/styles.scss b/app/institutions/dashboard/users/styles.scss new file mode 100644 index 00000000000..93ca0026725 --- /dev/null +++ b/app/institutions/dashboard/users/styles.scss @@ -0,0 +1,3 @@ +.panel-wrapper { + margin-top: 12px; +} diff --git a/app/institutions/dashboard/users/template.hbs b/app/institutions/dashboard/users/template.hbs new file mode 100644 index 00000000000..9bcdf36be8f --- /dev/null +++ b/app/institutions/dashboard/users/template.hbs @@ -0,0 +1,11 @@ + + +
    + +
    +
    +
    diff --git a/app/models/index-card.ts b/app/models/index-card.ts index 3f64d81888a..15fcd514373 100644 --- a/app/models/index-card.ts +++ b/app/models/index-card.ts @@ -16,6 +16,26 @@ export interface LanguageText { '@value': string; } +export enum OsfmapResourceTypes { + Project = 'Project', + ProjectComponent = 'ProjectComponent', + Registration = 'Registration', + RegistrationComponent = 'RegistrationComponent', + Preprint = 'Preprint', + File = 'File', + Person = 'Person', + Agent = 'Agent', + Organization = 'Organization', + Concept = 'Concept', + ConceptScheme = 'Concept:Scheme', +} + +export enum AttributionRoleIris { + Admin = 'osf:admin-contributor', + Write = 'osf:write-contributor', + Read = 'osf:readonly-contributor', +} + export default class IndexCardModel extends Model { @service intl!: IntlService; @@ -36,7 +56,8 @@ export default class IndexCardModel extends Model { } get osfModelType() { - const types = this.resourceMetadata.resourceType.map( (item: any) => item['@id']); + const types: OsfmapResourceTypes = this.resourceMetadata.resourceType + .map((item: Record<'@id', OsfmapResourceTypes>) => item['@id']); if (types.includes('Project') || types.includes('ProjectComponent')) { return 'node'; } else if (types.includes('Registration') || types.includes('RegistrationComponent')) { @@ -74,7 +95,7 @@ export default class IndexCardModel extends Model { async getOsfModel(options?: object) { const identifier = this.resourceIdentifier; if (identifier && this.osfModelType) { - const guid = this.guidFromIdentifierList(identifier); + const guid = this.osfGuid; if (guid) { const osfModel = await this.store.findRecord(this.osfModelType, guid, options); this.osfModel = osfModel; @@ -82,16 +103,16 @@ export default class IndexCardModel extends Model { } } - guidFromIdentifierList() { - for (const iri of this.resourceIdentifier) { - if (iri && iri.startsWith(osfUrl)) { - const pathSegments = iri.slice(osfUrl.length).split('/').filter(Boolean); - if (pathSegments.length === 1) { - return pathSegments[0]; // one path segment; looks like osf-id - } - } + get osfIdentifier() { + return this.resourceIdentifier.find(iri => iri.startsWith(osfUrl)) || ''; + } + + get osfGuid() { + const pathSegments = this.osfIdentifier.slice(osfUrl.length).split('/').filter(Boolean); + if (pathSegments.length === 1) { + return pathSegments[0]; // one path segment; looks like osf-id } - return null; + return ''; } } diff --git a/app/models/institution-summary-metric.ts b/app/models/institution-summary-metric.ts index ac3675fafe6..9e84ede5b0d 100644 --- a/app/models/institution-summary-metric.ts +++ b/app/models/institution-summary-metric.ts @@ -1,10 +1,23 @@ import { attr } from '@ember-data/model'; +import humanFileSize from 'ember-osf-web/utils/human-file-size'; import OsfModel from './osf-model'; export default class InstitutionSummaryMetricModel extends OsfModel { @attr('number') publicProjectCount!: number; @attr('number') privateProjectCount!: number; @attr('number') userCount!: number; + @attr('number') publicRegistrationCount!: number; + @attr('number') publishedPreprintCount!: number; + @attr('number') embargoedRegistrationCount!: number; + @attr('number') storageByteCount!: number; + @attr('number') publicFileCount!: number; + @attr('number') monthlyLoggedInUserCount!: number; + @attr('number') monthlyActiveUserCount!: number; + + + get convertedStorageCount(): string { + return humanFileSize(parseFloat(this.storageByteCount.toFixed(1))); + } } declare module 'ember-data/types/registries/model' { diff --git a/app/models/institution-user.ts b/app/models/institution-user.ts index 0ce79fde66a..2fb19f778d3 100644 --- a/app/models/institution-user.ts +++ b/app/models/institution-user.ts @@ -1,6 +1,6 @@ import { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model'; - import UserModel from 'ember-osf-web/models/user'; +import humanFileSize from 'ember-osf-web/utils/human-file-size'; import OsfModel from './osf-model'; @@ -9,6 +9,16 @@ export default class InstitutionUserModel extends OsfModel { @attr('fixstring') department?: string; @attr('number') publicProjects!: number; @attr('number') privateProjects!: number; + @attr('number') publicRegistrationCount!: number; + @attr('number') embargoedRegistrationCount!: number; + @attr('number') publishedPreprintCount!: number; + @attr('number') publicFileCount!: number; + @attr('number') storageByteCount!: number; + @attr('number') totalObjectCount!: number; + @attr('string') monthLastLogin!: string; // YYYY-MM + @attr('string') monthLastActive!: string; // YYYY-MM + @attr('string') accountCreationDate!: string; // YYYY-MM + @attr('fixstring') orcidId?: string; @belongsTo('user', { async: true }) user!: AsyncBelongsTo & UserModel; @@ -16,6 +26,10 @@ export default class InstitutionUserModel extends OsfModel { get userGuid() { return (this as InstitutionUserModel).belongsTo('user').id(); } + + get userDataUsage() { + return humanFileSize(this.storageByteCount); + } } declare module 'ember-data/types/registries/model' { diff --git a/app/models/institution.ts b/app/models/institution.ts index 9e2fa60ce7a..9b0432faf02 100644 --- a/app/models/institution.ts +++ b/app/models/institution.ts @@ -29,6 +29,7 @@ export default class InstitutionModel extends OsfModel { @attr('string') authUrl!: string; @attr('object') assets?: Assets; @attr('boolean', { defaultValue: false }) currentUserIsAdmin!: boolean; + @attr('fixstring') linkToExternalReportsArchive?: string; // only serialized when currentUserIsAdmin @attr('date') lastUpdated!: Date; @attr('fixstring') rorIri!: string; // identifier_domain in the admin app diff --git a/app/models/search-result.ts b/app/models/search-result.ts index 5d02d1c9c63..45a41c9c43c 100644 --- a/app/models/search-result.ts +++ b/app/models/search-result.ts @@ -2,6 +2,7 @@ import Model, { attr, belongsTo } from '@ember-data/model'; import { inject as service } from '@ember/service'; import { htmlSafe } from '@ember/template'; import IntlService from 'ember-intl/services/intl'; +import { getOsfmapValues, getSingleOsfmapObject, getSingleOsfmapValue } from 'ember-osf-web/packages/osfmap/jsonld'; import { languageFromLanguageCode } from 'osf-components/components/file-metadata-manager/component'; import IndexCardModel from './index-card'; @@ -20,6 +21,17 @@ export interface TextMatchEvidence { osfmapPropertyPath: string[]; } +export const CardLabelTranslationKeys = { + project: 'osf-components.search-result-card.project', + project_component: 'osf-components.search-result-card.project_component', + registration: 'osf-components.search-result-card.registration', + registration_component: 'osf-components.search-result-card.registration_component', + preprint: 'osf-components.search-result-card.preprint', + file: 'osf-components.search-result-card.file', + user: 'osf-components.search-result-card.user', + unknown: 'osf-components.search-result-card.unknown', +}; + export default class SearchResultModel extends Model { @service intl!: IntlService; @@ -51,22 +63,22 @@ export default class SearchResultModel extends Model { get displayTitle() { if (this.resourceType === 'user') { - return this.resourceMetadata['name'][0]['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['name']); } else if (this.resourceType === 'file') { - return this.resourceMetadata['fileName'][0]['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['fileName']); } - return this.resourceMetadata['title']?.[0]['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['title']); } get fileTitle() { if (this.resourceType === 'file') { - return this.resourceMetadata.title?.[0]['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['title']); } return null; } get description() { - return this.resourceMetadata.description?.[0]?.['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['description']); } get absoluteUrl() { @@ -75,19 +87,18 @@ export default class SearchResultModel extends Model { // returns list of affilated institutions for users // returns list of contributors for osf objects - // returns list of affiliated institutions for osf users get affiliatedEntities() { if (this.resourceType === 'user') { if (this.resourceMetadata.affiliation) { return this.resourceMetadata.affiliation.map((item: any) => - ({ name: item.name[0]['@value'], absoluteUrl: item['@id'] })); + ({ name: getSingleOsfmapValue(item, ['name']), absoluteUrl: item['@id'] })); } } else if (this.resourceMetadata.creator) { return this.resourceMetadata.creator?.map((item: any) => - ({ name: item.name[0]['@value'], absoluteUrl: item['@id'] })); + ({ name: getSingleOsfmapValue(item, ['name']), absoluteUrl: item['@id'] })); } else if (this.isContainedBy?.[0]?.creator) { return this.isContainedBy[0].creator.map((item: any) => - ({ name: item.name?.[0]?.['@value'], absoluteUrl: item['@id'] })); + ({ name: getSingleOsfmapValue(item, ['name']), absoluteUrl: item['@id'] })); } } @@ -100,22 +111,22 @@ export default class SearchResultModel extends Model { return [ { label: this.intl.t('osf-components.search-result-card.date_registered'), - date: this.resourceMetadata.dateCreated?.[0]['@value'], + date: getSingleOsfmapValue(this.resourceMetadata, ['dateCreated']), }, { label: this.intl.t('osf-components.search-result-card.date_modified'), - date: this.resourceMetadata.dateModified?.[0]['@value'], + date: getSingleOsfmapValue(this.resourceMetadata, ['dateModified']), }, ]; default: return [ { label: this.intl.t('osf-components.search-result-card.date_created'), - date: this.resourceMetadata.dateCreated?.[0]['@value'], + date: getSingleOsfmapValue(this.resourceMetadata, ['dateCreated']), }, { label: this.intl.t('osf-components.search-result-card.date_modified'), - date: this.resourceMetadata.dateModified?.[0]['@value'], + date: getSingleOsfmapValue(this.resourceMetadata, ['dateModified']), }, ]; } @@ -153,8 +164,8 @@ export default class SearchResultModel extends Model { const isPartOfCollection = this.resourceMetadata.isPartOfCollection; if (isPartOfCollection) { return { - title: this.resourceMetadata.isPartOfCollection?.[0]?.title?.[0]?.['@value'], - absoluteUrl: this.resourceMetadata.isPartOfCollection?.[0]?.['@id'], + title: getSingleOsfmapValue(this.resourceMetadata, ['isPartOfCollection', 'title']), + absoluteUrl: getSingleOsfmapValue(this.resourceMetadata, ['isPartOfCollection']), }; } return null; @@ -162,7 +173,8 @@ export default class SearchResultModel extends Model { get languageFromCode() { if (this.resourceMetadata.language) { - return languageFromLanguageCode(this.resourceMetadata.language[0]['@value']); + const language = getSingleOsfmapValue(this.resourceMetadata, ['language']); + return languageFromLanguageCode(language); } return null; } @@ -170,8 +182,8 @@ export default class SearchResultModel extends Model { get funders() { if (this.resourceMetadata.funder) { return this.resourceMetadata.funder.map( (item: any) => ({ - name: item.name[0]['@value'], - identifier: item.identifier?.[0]['@value'], + name: getSingleOsfmapValue(item, ['name']), + identifier: getSingleOsfmapValue(item, ['identifier']), })); } return null; @@ -180,8 +192,8 @@ export default class SearchResultModel extends Model { get nodeFunders() { if (this.resourceMetadata.isContainedBy?.[0]?.funder) { return this.resourceMetadata.isContainedBy[0].funder.map( (item: any) => ({ - name: item.name[0]['@value'], - identifier: item.identifier?.[0]['@value'], + name: getSingleOsfmapValue(item, ['name']), + identifier: getSingleOsfmapValue(item, ['identifier']), })); } return null; @@ -190,8 +202,8 @@ export default class SearchResultModel extends Model { get provider() { if (this.resourceMetadata.publisher) { return { - name: this.resourceMetadata.publisher[0]?.name?.[0]['@value'], - identifier: this.resourceMetadata.publisher[0]['@id'], + name: getSingleOsfmapValue(this.resourceMetadata, ['publisher', 'name']), + identifier: getSingleOsfmapValue(this.resourceMetadata, ['publisher']), }; } return null; @@ -204,8 +216,8 @@ export default class SearchResultModel extends Model { get license() { if (this.resourceMetadata.rights) { return { - name: this.resourceMetadata.rights?.[0]?.name?.[0]?.['@value'], - identifier: this.resourceMetadata.rights?.[0]?.['@id'], + name: getSingleOsfmapValue(this.resourceMetadata, ['rights', 'name']), + identifier: getSingleOsfmapValue(this.resourceMetadata, ['rights']), }; } return null; @@ -214,9 +226,9 @@ export default class SearchResultModel extends Model { get nodeLicense() { if (this.resourceMetadata.isContainedBy?.[0]?.rights) { return { - name: this.resourceMetadata.isContainedBy[0].rights?.[0]?.name?.[0]?.['@value'], - identifier: this.resourceMetadata.rights?.[0]?.['@id'] || - this.resourceMetadata.isContainedBy[0].rights[0]?.['@id'], + name: getSingleOsfmapValue(this.resourceMetadata, ['isContainedBy', 'rights', 'name']), + identifier: getSingleOsfmapValue(this.resourceMetadata, ['rights']) || + getSingleOsfmapValue(this.resourceMetadata, ['isContainedBy', 'rights']), }; } return null; @@ -242,6 +254,10 @@ export default class SearchResultModel extends Model { return 'unknown'; } + get intlResourceType() { + return this.intl.t(CardLabelTranslationKeys[this.resourceType]); + } + get orcids() { if (this.resourceMetadata.identifier) { const orcids = this.resourceMetadata.identifier.filter( @@ -253,7 +269,7 @@ export default class SearchResultModel extends Model { } get resourceNature() { - return this.resourceMetadata.resourceNature?.[0]?.displayLabel?.[0]?.['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['resourceNature','displayLabel']); } get hasDataResource() { @@ -277,12 +293,32 @@ export default class SearchResultModel extends Model { } get registrationTemplate() { - return this.resourceMetadata.conformsTo?.[0]?.title?.[0]?.['@value']; + return getSingleOsfmapValue(this.resourceMetadata, ['conformsTo', 'title']); } get isWithdrawn() { return this.resourceMetadata.dateWithdrawn || this.resourceMetadata['https://osf.io/vocab/2022/withdrawal']; } + + get configuredAddonNames() { + return getOsfmapValues(this.resourceMetadata, ['hasOsfAddon', 'prefLabel']); + } + + get storageRegion() { + return getSingleOsfmapValue(this.resourceMetadata, ['storageRegion', 'prefLabel']); + } + + get usageMetrics() { + const usage = getSingleOsfmapObject(this.resourceMetadata, ['usage']); + if (!usage) { + return null; + } + return { + period: getSingleOsfmapValue(usage, ['temporalCoverage']), + viewCount: getSingleOsfmapValue(usage, ['viewCount']), + downloadCount: getSingleOsfmapValue(usage, ['downloadCount']), + }; + } } declare module 'ember-data/types/registries/model' { diff --git a/app/packages/osfmap/jsonld.ts b/app/packages/osfmap/jsonld.ts new file mode 100644 index 00000000000..9711ea177ca --- /dev/null +++ b/app/packages/osfmap/jsonld.ts @@ -0,0 +1,37 @@ +export function *iterOsfmapObjects(osfmapObject: any, propertyPath: string[]): IterableIterator { + const [property, ...remainingPath] = propertyPath; + const innerObjArray = osfmapObject[property] || []; + if (remainingPath.length) { + for (const innerObj of innerObjArray) { + yield* iterOsfmapObjects(innerObj, remainingPath); + } + } else { + yield* innerObjArray; + } +} + +export function *iterOsfmapValues(osfmapObject: any, propertyPath: string[]): IterableIterator { + for (const obj of iterOsfmapObjects(osfmapObject, propertyPath)) { + yield (Object.hasOwn(obj, '@id') ? obj['@id'] : obj['@value']); + } +} + +export function getOsfmapValues(osfmapObject: any, propertyPath: string[]) { + return Array.from(iterOsfmapValues(osfmapObject, propertyPath)); +} + +export function getSingleOsfmapValue(osfmapObject: any, propertyPath: string[]) { + return iterOsfmapValues(osfmapObject, propertyPath).next().value; +} + +export function getOsfmapObjects(osfmapObject: any, propertyPath: string[]) { + return Array.from(iterOsfmapObjects(osfmapObject, propertyPath)); +} + +export function getSingleOsfmapObject(osfmapObject: any, propertyPath: string[]) { + return iterOsfmapObjects(osfmapObject, propertyPath).next().value; +} + +export function hasOsfmapValue(osfmapObject: any, propertyPath: string[], expectedValue: any) { + return Array.from(iterOsfmapValues(osfmapObject, propertyPath)).some(value => value === expectedValue); +} diff --git a/app/router.ts b/app/router.ts index 02a2f8aa6c3..be2416a6e37 100644 --- a/app/router.ts +++ b/app/router.ts @@ -23,7 +23,12 @@ Router.map(function() { this.route('search'); this.route('institutions', function() { this.route('discover', { path: '/:institution_id' }); - this.route('dashboard', { path: '/:institution_id/dashboard' }); + this.route('dashboard', { path: '/:institution_id/dashboard' }, function() { + this.route('projects'); + this.route('registrations'); + this.route('preprints'); + this.route('users'); + }); }); this.route('preprints', function() { diff --git a/app/styles/_components.scss b/app/styles/_components.scss index 7aa123fa52c..0e1db0303ad 100644 --- a/app/styles/_components.scss +++ b/app/styles/_components.scss @@ -902,3 +902,38 @@ button.nav-user-dropdown { .logoutLink { cursor: pointer; } + +@mixin tab-list { + margin-bottom: 10px; + border-bottom: 1px solid $color-border-gray; + box-sizing: border-box; + color: $color-text-black; + display: block; + line-height: 20px; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + height: 41px; + padding: 0; + + li { + display: block; + position: relative; + margin-bottom: -1px; + float: left; + height: 41px; + padding: 10px 15px; + } + + li.ember-tabs__tab--selected { + background-color: $bg-light; + border-bottom: 2px solid $color-blue; + } + + li:hover { + border-color: transparent; + text-decoration: none; + background-color: $bg-light; + color: var(--primary-color); + } +} diff --git a/app/styles/_variables.scss b/app/styles/_variables.scss index adcc9ab19ff..06441451109 100644 --- a/app/styles/_variables.scss +++ b/app/styles/_variables.scss @@ -130,6 +130,7 @@ $color-grey: #333; $color-filter-bg: #a4b3bd; $color-red: #f00; $color-green: #090; +$color-green-light: #90ee90; $color-yellow: #ff0; $color-turquoise: rgb(64, 224, 211); $color-purple: rgb(154, 0, 192); diff --git a/ember-cli-build.js b/ember-cli-build.js index ebbc8fe6086..b6b9e0abb1e 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -32,7 +32,9 @@ module.exports = function(defaults) { ], }, 'ember-composable-helpers': { - only: ['compose', 'contains', 'flatten', 'includes', 'range', 'queue', 'map-by', 'without', 'find-by'], + only: [ + 'call', 'compose', 'contains', 'find-by', 'flatten', 'includes', 'map-by', 'queue', 'range', 'without', + ], }, fingerprint: { enabled: true, diff --git a/lib/app-components/addon/components/project-contributors/list/item/template.hbs b/lib/app-components/addon/components/project-contributors/list/item/template.hbs index d7b13fa1c58..4211d83e60b 100644 --- a/lib/app-components/addon/components/project-contributors/list/item/template.hbs +++ b/lib/app-components/addon/components/project-contributors/list/item/template.hbs @@ -48,11 +48,11 @@ @selected={{@contributor.permission}} as |option| > - {{t (concat 'app_components.project_contributors.list.item.permissions.' option)}} + {{t (concat 'general.permissions.' option)}} {{else}}
    - {{t (concat 'app_components.project_contributors.list.item.permissions.' @contributor.permission)}} + {{t (concat 'general.permissions.' @contributor.permission)}}
    {{/if}} diff --git a/lib/osf-components/addon/components/adjustable-paginator/component.ts b/lib/osf-components/addon/components/adjustable-paginator/component.ts new file mode 100644 index 00000000000..fedf34a6bad --- /dev/null +++ b/lib/osf-components/addon/components/adjustable-paginator/component.ts @@ -0,0 +1,92 @@ +import { classNames, tagName } from '@ember-decorators/component'; +import Component from '@ember/component'; +import { action, computed } from '@ember/object'; +import { gt } from '@ember/object/computed'; +import { layout } from 'ember-osf-web/decorators/component'; +import styles from './styles'; +import template from './template'; + +@layout(template, styles) +@tagName('span') +@classNames('sort-group') +export default class AdjustablePaginator extends Component { + page?: number; + maxPage?: number; + totalCount?: number; + previousPage?: () => unknown; + nextPage?: () => unknown; + selectedPageSize = 10; + + defaultPageSizeOptions = [10, 25, 50, 100]; + + @computed('totalCount', 'defaultPageSizeOptions') + get pageSizeOptions(): number[] { + if (this.totalCount) { + // Filter options smaller or equal to totalCount and include the next higher option + const filteredOptions = this.defaultPageSizeOptions.filter(option => option <= this.totalCount); + + // Find the first option greater than totalCount and include it as well + const nextHigherOption = this.defaultPageSizeOptions.find(option => option > this.totalCount); + + if (nextHigherOption) { + filteredOptions.push(nextHigherOption); // Include the next higher option + } + + return filteredOptions; + } + + return this.defaultPageSizeOptions; + } + + @computed('page', 'maxPage') + get hasNext(): boolean { + return Boolean(this.page && this.maxPage && this.page < this.maxPage); + } + @computed('page') + get prevPage(): number { + return this.page - 1; + } + + @computed('page') + get nextPage2(): number { + return this.page + 1; + } + + @computed('maxPage') + get finalPage(): number { + return this.maxPage + 1; + } + + @gt('page', 1) hasPrev!: boolean; + + @gt('maxPage', 1) hasMultiplePages!: boolean; + + @action + _previous() { + if (this.previousPage) { + this.previousPage(); + } + } + + @action + _next() { + if (this.nextPage) { + this.nextPage(); + } + } + + @action + onPageSizeChange(value: int) { + this.set('pageSize', value); + if (this.doReload) { + this.doReload(); + } + } + + @action + setPage(page: number) { + if (this.doReload) { + this.doReload(page); + } + } +} diff --git a/lib/osf-components/addon/components/adjustable-paginator/styles.scss b/lib/osf-components/addon/components/adjustable-paginator/styles.scss new file mode 100644 index 00000000000..066079586da --- /dev/null +++ b/lib/osf-components/addon/components/adjustable-paginator/styles.scss @@ -0,0 +1,76 @@ +.paginator__control { + display: inline-flex; + align-items: center; + margin: 0; + + &:first-of-type { + padding-left: 0; + } +} + +.paginator__button, +.paginator__select { + background-color: $color-bg-white; + border: $border-light; + color: $color-text-blue-dark; + padding: 7px 16px; + font-size: 14px; + border-radius: $radius; + cursor: pointer; + transition: background-color 0.3s ease, border-color 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + height: 40px; // Setting height to ensure the same for both buttons and select + box-sizing: border-box; // Ensures padding and borders are included in the height + + &:hover { + background-color: $color-bg-gray-light; + border-color: $color-border-gray-dark; + } + + &:disabled { + color: $color-text-blue-dark; + background-color: $bg-light; + cursor: not-allowed; + border-color: $color-border-gray; + } + + &:focus { + outline: none; + border-color: $secondary-blue; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } +} + +.paginator__button--current { + background-color: $color-bg-gray-lighter; + border: 1px solid $color-osf-primary; + color: $color-text-blue-dark; + font-weight: bold; + cursor: default; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* Special styling for next/previous buttons */ +.paginator__button--prev, +.paginator__button--next { + font-weight: bolder; + padding: 5px 12px; +} + +.paginator__select { + max-width: 160px; + margin-top: -5px; + font-weight: bolder; +} + +.paginator__ellipsis { + color: $color-text-blue-dark; + margin: 0 8px; + font-size: 14px; + display: inline-flex; + align-items: center; +} diff --git a/lib/osf-components/addon/components/adjustable-paginator/template.hbs b/lib/osf-components/addon/components/adjustable-paginator/template.hbs new file mode 100644 index 00000000000..4d04950503e --- /dev/null +++ b/lib/osf-components/addon/components/adjustable-paginator/template.hbs @@ -0,0 +1,140 @@ + +
    + +
    +
    + + +{{#if this.hasMultiplePages}} + + + + + {{!-- Always show the first page button --}} + + + + + {{#if (lte this.maxPage 3)}} + {{!-- If fewer than 3 pages, show all pages --}} + {{#each (range 2 3) as |page|}} + + + + {{/each}} + {{else}} + {{#if (gt this.prevPage 2)}} + + + + {{/if}} + + {{!-- Conditionally show previous and current pages --}} + {{#if (not (eq this.prevPage 1))}} + {{#if (not (eq this.page 1))}} + + + + {{/if}} + {{/if}} + + {{#if (not (eq this.page 1))}} + + + + {{/if}} + + {{!-- Show nextPage only if it differs from maxPage --}} + {{#if (and this.hasNext (not (eq this.nextPage2 this.maxPage)))}} + + + + {{/if}} + + {{#if (not (gte this.nextPage2 this.maxPage))}} + {{#if (not (and (eq this.page 3) (eq this.maxPage 5))) }} + + + + {{/if}} + {{/if}} + + {{!-- Always show the maxPage button --}} + {{#if (not (eq this.page this.maxPage))}} + + + + {{/if}} + {{/if}} + + + + +{{/if}} diff --git a/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs b/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs index a64301b3c5c..be16b23125c 100644 --- a/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs +++ b/lib/osf-components/addon/components/contributors/add-unregistered-modal/template.hbs @@ -42,7 +42,7 @@ data-test-select-permission as |permission| > - {{t (concat 'osf-components.contributors.permissions.' permission)}} + {{t (concat 'general.permissions.' permission)}} {{#let (unique-id 'citation-checkbox') as |id|}}
  • +
    + + + + + + +
    \ No newline at end of file diff --git a/app/preprints/-components/submit/preprint-state-machine/styles.scss b/app/preprints/-components/submit/preprint-state-machine/styles.scss new file mode 100644 index 00000000000..ca382e9984e --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/styles.scss @@ -0,0 +1,35 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +$container-width: 1144px; +$side-container-width: 205px; +$middle-container-width: $container-width - $side-container-width - $side-container-width; +$page-height: 1000px; + +.preprint-state-machine-container { + height: $page-height; + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-size: 16px; + + .flow-status-container { + height: $page-height; + width: $side-container-width; + } + + .flow-input-container { + height: $page-height; + width: $middle-container-width; + } + + .flow-action-container { + height: $page-height; + width: $side-container-width; + } + + &.mobile { + height: fit-content; + } +} diff --git a/app/preprints/-components/submit/preprint-state-machine/template.hbs b/app/preprints/-components/submit/preprint-state-machine/template.hbs new file mode 100644 index 00000000000..60ffb3451ab --- /dev/null +++ b/app/preprints/-components/submit/preprint-state-machine/template.hbs @@ -0,0 +1,41 @@ +{{yield (hash + onDelete=this.onDelete + onWithdrawal=this.onWithdrawal + onClickStep=this.onClickStep + addProjectFile=this.addProjectFile + onNext=this.onNext + onPrevious=this.onPrevious + onSubmit=this.onSubmit + preprint=this.preprint + provider=this.provider + isNextButtonDisabled=this.isNextButtonDisabled + isPreviousButtonDisabled=this.isPreviousButtonDisabled + isEditFlow=this.isEditFlow + isDeleteButtonDisplayed=this.isDeleteButtonDisplayed + isWithdrawalButtonDisplayed=this.isWithdrawalButtonDisplayed + + getTitleAndAbstractType=this.getTitleAndAbstractType + getFileType=this.getFileType + getMetadataType=this.getMetadataType + getAuthorAssertionsType=this.getAuthorAssertionsType + getSupplementsType=this.getSupplementsType + getReviewType=this.getReviewType + + validateTitleAndAbstract=this.validateTitleAndAbstract + validateFile=this.validateFile + validateMetadata=this.validateMetadata + validateAuthorAssertions=this.validateAuthorAssertions + validateSupplements=this.validateSupplements + + shouldDisplayStatusType=this.shouldDisplayStatusType + getStatusTitle=this.getStatusTitle + isSelected=this.isSelected + isFinished=this.isFinished + isDisabled=this.isDisabled + onClick=this.onClick + getAnalytics=this.getAnalytics + getFaIcon=this.getFaIcon + + statusFlowIndex=this.statusFlowIndex + displayAuthorAssertions=this.displayAuthorAssertions +)}} \ No newline at end of file diff --git a/app/preprints/-components/submit/review/component.ts b/app/preprints/-components/submit/review/component.ts new file mode 100644 index 00000000000..f59384965f1 --- /dev/null +++ b/app/preprints/-components/submit/review/component.ts @@ -0,0 +1,69 @@ +import Store from '@ember-data/store'; +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import PreprintStateMachine from 'ember-osf-web/preprints/-components/submit/preprint-state-machine/component'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import moment from 'moment-timezone'; +import Intl from 'ember-intl/services/intl'; + +/** + * The Review Args + */ +interface ReviewArgs { + manager: PreprintStateMachine; +} + +/** + * The Review Component + */ +export default class Review extends Component{ + @service store!: Store; + @tracked preprint = this.args.manager.preprint; + @tracked provider?: any; + @tracked license?: any; + @tracked contributors?: any; + @tracked subjects?: any; + @service intl!: Intl; + + constructor(owner: unknown, args: ReviewArgs) { + super(owner, args); + + taskFor(this.loadPreprint).perform(); + } + + @task + @waitFor + private async loadPreprint() { + this.provider = this.preprint.provider.content; + this.license = this.preprint.license; + this.subjects = await this.preprint.queryHasMany('subjects'); + } + + public get providerLogo(): string | undefined { + return this.provider.get('assets')?.square_color_no_transparent; + } + + public get displayPublicationDoi(): string { + return this.preprint.articleDoiUrl || this.intl.t('general.not-applicable'); + } + + public get displayPublicationDate(): string { + return this.preprint.originalPublicationDate + ? moment(this.preprint.originalPublicationDate).format('YYYY-MM-DD') + : this.intl.t('general.not-applicable'); + } + + public get displayPublicationCitation(): string { + return this.preprint.customPublicationCitation + ? this.preprint.customPublicationCitation + : this.intl.t('general.not-applicable'); + } + + public get providerServiceLabel(): string { + return this.intl.t('preprints.submit.step-review.preprint-service', + { singularPreprintWord: this.provider.documentType.singularCapitalized }); + } +} diff --git a/app/preprints/-components/submit/review/styles.scss b/app/preprints/-components/submit/review/styles.scss new file mode 100644 index 00000000000..0acb46ee04d --- /dev/null +++ b/app/preprints/-components/submit/review/styles.scss @@ -0,0 +1,67 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-input-container { + width: 100%; + + .step-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .title { + font-weight: bold; + } + + .content-container { + width: 100%; + margin-top: 20px; + + h4 { + margin-top: 10px; + margin-bottom: 10px; + font-weight: bold; + } + + .display { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + &.ellipsis { + text-overflow: ellipsis; + overflow: hidden; + white-space: normal; + } + + .image { + width: 30px; + height: 30px; + margin-right: 10px; + } + + .text { + height: 30px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + } + } + } + + hr { + margin-top: 20px; + margin-bottom: 20px; + width: 100%; + border: 1px solid $color-border-gray; + } + } + + &.mobile { + height: fit-content; + } +} diff --git a/app/preprints/-components/submit/review/template.hbs b/app/preprints/-components/submit/review/template.hbs new file mode 100644 index 00000000000..8761d944e74 --- /dev/null +++ b/app/preprints/-components/submit/review/template.hbs @@ -0,0 +1,140 @@ +
    + {{#if this.loadPreprint.isRunning}} + + {{else}} +
    +

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    + {{#unless this.isSupplementAttached}} +

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

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

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

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

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

    + {{this.model.preprint.customPublicationCitation}} +
    + {{/if}} diff --git a/app/preprints/edit/controller.ts b/app/preprints/edit/controller.ts new file mode 100644 index 00000000000..3c41eb6e790 --- /dev/null +++ b/app/preprints/edit/controller.ts @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { action} from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class PreprintEdit extends Controller { + @tracked isPageDirty = false; + + @action + setPageDirty() { + this.isPageDirty = true; + } + + @action + resetPageDirty() { + this.isPageDirty = false; + } +} diff --git a/app/preprints/edit/route.ts b/app/preprints/edit/route.ts new file mode 100644 index 00000000000..9773c596930 --- /dev/null +++ b/app/preprints/edit/route.ts @@ -0,0 +1,95 @@ +import Store from '@ember-data/store'; +import Route from '@ember/routing/route'; +import RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; +// eslint-disable-next-line ember/no-mixins +import ConfirmationMixin from 'ember-onbeforeunload/mixins/confirmation'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import MetaTags, { HeadTagDef } from 'ember-osf-web/services/meta-tags'; +import Theme from 'ember-osf-web/services/theme'; +import requireAuth from 'ember-osf-web/decorators/require-auth'; +import { action, computed } from '@ember/object'; +import PreprintEdit from 'ember-osf-web/preprints/edit/controller'; +import Intl from 'ember-intl/services/intl'; +import Transition from '@ember/routing/-private/transition'; +import { Permission } from 'ember-osf-web/models/osf-model'; + +@requireAuth() +export default class PreprintEditRoute extends Route.extend(ConfirmationMixin, {}) { + @service store!: Store; + @service theme!: Theme; + @service router!: RouterService; + @service intl!: Intl; + @service metaTags!: MetaTags; + headTags?: HeadTagDef[]; + + // This does NOT work on chrome and I'm going to leave it just in case + confirmationMessage = this.intl.t('preprints.submit.action-flow.save-before-exit'); + + buildRouteInfoMetadata() { + return { + osfMetrics: { + providerId: this.theme.id, + }, + }; + } + + async model(args: any) { + try { + const provider = await this.store.findRecord('preprint-provider', args.provider_id); + this.theme.providerType = 'preprint'; + this.theme.id = args.provider_id; + + const preprint = await this.store.findRecord('preprint', args.guid); + + if ( + !preprint.currentUserPermissions.includes(Permission.Write) || + preprint.isWithdrawn + ) { + throw new Error('User does not have permission to edit this preprint'); + } + + + return { + provider, + preprint, + brand: provider.brand.content, + }; + } catch (e) { + this.router.transitionTo('not-found', `preprints/${args.provider_id}`); + return null; + } + } + + afterModel(model: PreprintProviderModel) { + if (model && model.assets && model.assets.favicon) { + const headTags = [{ + type: 'link', + attrs: { + rel: 'icon', + href: model.assets.favicon, + }, + }]; + this.set('headTags', headTags); + } + } + + // This tells ember-onbeforeunload's ConfirmationMixin whether or not to stop transitions + // This is for when the user leaves the site or does a full app reload + @computed('controller.isPageDirty') + get isPageDirty() { + const controller = this.controller as PreprintEdit; + return () => controller.isPageDirty; + } + + // This is for when the user leaves the page via the router + @action + willTransition(transition: Transition) { + const controller = this.controller as PreprintEdit; + if (controller.isPageDirty) { + if (!window.confirm(this.intl.t('preprints.submit.action-flow.save-before-exit'))) { + transition.abort(); + } + } + } +} diff --git a/app/preprints/edit/template.hbs b/app/preprints/edit/template.hbs new file mode 100644 index 00000000000..74f4681d55c --- /dev/null +++ b/app/preprints/edit/template.hbs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/app/preprints/index/controller.ts b/app/preprints/index/controller.ts index b7dc6e2c6e8..59f55cfa839 100644 --- a/app/preprints/index/controller.ts +++ b/app/preprints/index/controller.ts @@ -4,22 +4,17 @@ import { action } from '@ember/object'; import RouterService from '@ember/routing/router-service'; import { inject as service } from '@ember/service'; import Theme from 'ember-osf-web/services/theme'; -import Media from 'ember-responsive'; import Intl from 'ember-intl/services/intl'; +import config from 'ember-osf-web/config/environment'; export default class Preprints extends Controller { @service store!: Store; @service theme!: Theme; @service router!: RouterService; - @service media!: Media; @service intl!: Intl; - get isMobile(): boolean { - return this.media.isMobile; - } - - get isOsf(): boolean { - return this.theme?.provider?.id === 'osf'; + get isDefaultProvider(): boolean { + return this.theme?.provider?.id === config.defaultProvider; } @action diff --git a/app/preprints/index/template.hbs b/app/preprints/index/template.hbs index 216f45a12ee..514fa792490 100644 --- a/app/preprints/index/template.hbs +++ b/app/preprints/index/template.hbs @@ -1,4 +1,4 @@ -
    @@ -155,8 +155,8 @@ {{!ADVISORY GROUP}} {{#if this.theme.provider.advisoryBoard.length}}
    {{html-safe this.theme.provider.advisoryBoard}} diff --git a/app/preprints/select/route.ts b/app/preprints/select/route.ts new file mode 100644 index 00000000000..6942843fddc --- /dev/null +++ b/app/preprints/select/route.ts @@ -0,0 +1,26 @@ +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import Store from '@ember-data/store'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +import requireAuth from 'ember-osf-web/decorators/require-auth'; +import Theme from 'ember-osf-web/services/theme'; +import config from 'ember-osf-web/config/environment'; + +@requireAuth() +export default class PreprintSelectRoute extends Route { + @service store!: Store; + @service theme!: Theme; + + async model(){ + const submissionProviders: PreprintProviderModel[] = await this.store.findAll('preprint-provider', { + reload: true, + adapterOptions: { 'filter[allowSubmissions]': 'true' }, + }); + + this.theme.set('id', config.defaultProvider); + + return { + submissionProviders, + }; + } +} diff --git a/app/preprints/select/styles.scss b/app/preprints/select/styles.scss new file mode 100644 index 00000000000..083f8f071e6 --- /dev/null +++ b/app/preprints/select/styles.scss @@ -0,0 +1,43 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +@import 'app/styles/layout'; + +.select-page-container { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .header-container { + padding: 30px 0; + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + background: url('assets/images/default-brand/bg-dark.jpg') top center $color-bg-color-grey; + + .header-clamp-width-container { + @include clamp-width; + + .header { + margin: 5px 10px; + font-size: 48px; + color: $color-text-white; + } + } + } + + &.mobile { + .header-container { + text-align: center; + + .header-clamp-width-container { + .header { + font-size: 36px; + } + } + } + } +} diff --git a/app/preprints/select/template.hbs b/app/preprints/select/template.hbs new file mode 100644 index 00000000000..def36ced049 --- /dev/null +++ b/app/preprints/select/template.hbs @@ -0,0 +1,15 @@ +{{page-title (t 'preprints.select.page-title')}} + +
    +
    +
    +

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

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

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

    -{{else}} - {{t 'registries.registration_metadata.add_contributors.help_text' htmlSafe=true}} {{/if}} diff --git a/lib/osf-components/addon/components/contributors/user-search/widget/component.ts b/lib/osf-components/addon/components/contributors/user-search/widget/component.ts index 3d6e1ee58b8..0e0a609c10f 100644 --- a/lib/osf-components/addon/components/contributors/user-search/widget/component.ts +++ b/lib/osf-components/addon/components/contributors/user-search/widget/component.ts @@ -1,5 +1,5 @@ import Store from '@ember-data/store'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; import Component from '@glimmer/component'; @@ -27,6 +27,7 @@ export default class UserSearchComponent extends Component -
    - - -
    - -
    -
    - - {{t 'registries.registration_metadata.add_contributors.results_heading'}} - +
    +
    +
    - +
    + +
    + {{#if this.displayResults}} +
    + +
    +
    + +
    + {{/if}}
    diff --git a/lib/osf-components/addon/components/contributors/widget/styles.scss b/lib/osf-components/addon/components/contributors/widget/styles.scss index 21128c9ca58..bcd2c34f678 100644 --- a/lib/osf-components/addon/components/contributors/widget/styles.scss +++ b/lib/osf-components/addon/components/contributors/widget/styles.scss @@ -1,31 +1,68 @@ -.Container { +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.warning-container { + margin-top: 10px; + color: $brand-danger; +} + +.display-container { border: 1px solid $color-border-gray; margin-top: 10px; overflow-y: scroll; + overflow-x: hidden; max-height: 500px; -} -.Heading { - padding: 11px 20px; - height: 40px; - font-weight: bold; - display: flex; - border-bottom: 1px solid $color-border-gray; - background-color: #fff; - position: sticky; - top: 0; - z-index: 1; + .heading-container { + width: 100%; + height: 40px; + font-weight: bold; + border-bottom: 1px solid $color-border-gray; + background-color: #fff; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + + .name-title, + .permission-title, + .citation-title { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .name-title { + padding-left: 40px; + width: 50%; + } + + .permission-title { + width: 30%; + } + + .citation-title { + width: 20%; + } + + &.mobile { + .name-title { + padding-left: 5px; + width: 100%; + } + } + } } -.HeadingTitle { - flex: 1 0 auto; - max-width: 30%; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; +.add-user-container { + margin-top: 10px; + + .user-search-container { + margin-top: 10px; + max-height: 425px; + overflow: hidden; + } - &:first-of-type { - flex: 2 0 auto; - max-width: 41%; + &.mobile { + max-height: 450px; } } diff --git a/lib/osf-components/addon/components/contributors/widget/template.hbs b/lib/osf-components/addon/components/contributors/widget/template.hbs index de9972815bb..16b79d578de 100644 --- a/lib/osf-components/addon/components/contributors/widget/template.hbs +++ b/lib/osf-components/addon/components/contributors/widget/template.hbs @@ -1,34 +1,44 @@ - {{#let (unique-id 'current-contributors') as |currentContributorFieldId|}} - {{#if @shouldShowAdd}} - - - {{/if}} -
    -
    - - {{t 'osf-components.contributors.headings.name'}} - - + {{#if @displayPermissionWarning}} +
    + {{t 'osf-components.contributors.permission-warning'}} +
    + {{/if}} +
    +
    +
    + {{t 'osf-components.contributors.headings.name'}} +
    + {{#if (not (is-mobile))}} +
    {{t 'osf-components.contributors.headings.permission'}} - - +
    +
    {{t 'osf-components.contributors.headings.citation'}} - +
    + {{/if}} +
    + +
    + {{#if @shouldShowAdd}} +
    + +
    +
    -
    - {{/let}} + {{/if}} diff --git a/lib/osf-components/addon/components/delete-button/component.ts b/lib/osf-components/addon/components/delete-button/component.ts index 8b7af271d2b..33ffb9ae8f3 100644 --- a/lib/osf-components/addon/components/delete-button/component.ts +++ b/lib/osf-components/addon/components/delete-button/component.ts @@ -29,6 +29,7 @@ export default class DeleteButton extends Component { // Optional arguments small = false; smallSecondary = false; + buttonLayout = 'medium'; noBackground = false; hardConfirm = false; disabled = false; diff --git a/lib/osf-components/addon/components/delete-button/template.hbs b/lib/osf-components/addon/components/delete-button/template.hbs index 868fbadf1ee..dbf45900ae8 100644 --- a/lib/osf-components/addon/components/delete-button/template.hbs +++ b/lib/osf-components/addon/components/delete-button/template.hbs @@ -7,6 +7,7 @@ @type='destroy' {{on 'click' this._show}} @disabled={{this.disabled}} + ...attributes > @@ -19,6 +20,7 @@ @type='secondary' @layout='small' {{on 'click' this._show}} + ...attributes > {{#if @icon}} @@ -29,9 +31,11 @@ diff --git a/lib/osf-components/addon/components/dropzone-widget/component.ts b/lib/osf-components/addon/components/dropzone-widget/component.ts index 3848d9db7d9..5eb14876a99 100644 --- a/lib/osf-components/addon/components/dropzone-widget/component.ts +++ b/lib/osf-components/addon/components/dropzone-widget/component.ts @@ -79,6 +79,7 @@ export default class DropzoneWidget extends Component.extend({ defaultMessage = this.intl.t('dropzone_widget.drop_files'); @requiredAction buildUrl!: (files: File[]) => void; + success?: (context: any, drop: any, file: any) => Promise; preUpload?: (context: any, drop: any, file: any) => Promise; didInsertElement() { diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts b/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts index aa5f8ba093f..819e68903fe 100644 --- a/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/component.ts @@ -1,38 +1,36 @@ import { tagName } from '@ember-decorators/component'; import Component from '@ember/component'; import { assert } from '@ember/debug'; -import { action } from '@ember/object'; import { BufferedChangeset } from 'ember-changeset/types'; import { layout } from 'ember-osf-web/decorators/component'; +import { SchemaBlock } from 'ember-osf-web/packages/registration-schema'; import styles from './styles'; import template from './template'; +export interface RadioButtonOption { + displayText: string; + inputValue: string | boolean | number; +} + @tagName('') @layout(template, styles) -export default class FormControlRadioButton extends Component { +export default class FormControlRadioButtonGroup extends Component { // Required params - options!: string[]; + options!: string[] | SchemaBlock[]; valuePath!: string; changeset!: BufferedChangeset; // Optional params + helpTextMapping?: any; shouldShowMessages?: boolean; disabled = false; - onchange?: (option: string) => void; + onchange?: (option: string | number | boolean) => void; didReceiveAttrs() { assert('FormControls::RadioButton - @options are required', Boolean(this.options)); assert('FormControls::RadioButton - @valuePath is required', Boolean(this.valuePath)); assert('FormControls::RadioButton - @changeset is required', Boolean(this.changeset)); } - - @action - updateChangeset(option: string) { - this.changeset.set(this.valuePath, option); - if (this.onchange) { - this.onchange(option); - } - } } diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/component.ts b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/component.ts new file mode 100644 index 00000000000..2e9c939a5ae --- /dev/null +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/component.ts @@ -0,0 +1,47 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { BufferedChangeset } from 'ember-changeset/types'; + +import { RadioButtonOption } from 'osf-components/components/form-controls/radio-button-group/component'; + +/** + * The Radio Button Args + */ +interface RadioButtonArgs{ + option: string | RadioButtonOption; + valuePath: string; + changeset: BufferedChangeset; + disabled: boolean; + helpTextMapping?: any; + onchange?: (_: string | number | boolean) => void; +} + +export default class FormControlRadioButton extends Component { + public get displayText(): string | number | boolean { + if (typeof this.args.option === 'string') { + return this.args.option; + } else { + return this.args.option.displayText !== undefined ? this.args.option.displayText : ''; + } + } + + public get isValueChecked(): boolean { + return this.args.changeset.get(this.args.valuePath) === this.getValue; + } + + public get getValue(): string | number | boolean { + if (typeof this.args.option === 'string') { + return this.args.option; + } else { + return this.args.option.inputValue !== undefined ? this.args.option.inputValue : ''; + } + } + + @action + public updateChangeset(): void { + this.args.changeset.set(this.args.valuePath, this.getValue); + if (this.args.onchange) { + this.args.onchange(this.getValue); + } + } +} diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/styles.scss b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/styles.scss new file mode 100644 index 00000000000..73037f48038 --- /dev/null +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/styles.scss @@ -0,0 +1,13 @@ +.RadioButton { + display: flex; + + .input { + flex: 0 0 auto; + } + + .RadioLabel { + flex: 1 0 0; + margin-left: 10px; + font-weight: 500; + } +} diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/template.hbs b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/template.hbs new file mode 100644 index 00000000000..ded859ab573 --- /dev/null +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/radio-button/template.hbs @@ -0,0 +1,22 @@ +
    + {{#let (unique-id 'radio' ) as |uniqueId|}} + + + {{/let}} +
    \ No newline at end of file diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss b/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss index bb988db496b..87b17f9c150 100644 --- a/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/styles.scss @@ -1,20 +1,12 @@ -.RadioButtonGroup { - padding-left: 10px; -} +// stylelint-disable max-nesting-depth, selector-max-compound-selectors -.RadioButton { +.RadioButtonContainer { display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; - input { - flex: 0 0 auto; - } - - label { - flex: 1 0 0; - margin-left: 10px; + .RadioButtonGroup { + padding-left: 10px; } } - -.RadioLabel { - font-weight: 500; -} diff --git a/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs b/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs index e87153edd06..292781d887f 100644 --- a/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs +++ b/lib/osf-components/addon/components/form-controls/radio-button-group/template.hbs @@ -1,33 +1,26 @@ -
    - {{#each this.options as |option|}} - {{#let (unique-id 'radio' option) as |uniqueId|}} +
    +
    + {{#each this.options as |option|}}
    - - +
    - {{/let}} - {{/each}} + {{/each}} +
    {{#if @shouldShowMessages}} void; +} + +export default class NodePicker extends Component { + @service currentUser!: CurrentUser; + @service store!: Store; + + @tracked selected: Node | null = null; + filter = ''; + page = 1; + @tracked hasMore = false; + @tracked loadingMore = false; + @tracked items: Node[] = []; + + constructor(owner: unknown, args: NodePickerArgs) { + super(owner, args); + + this.selected = null; + this.filter = ''; + this.page = 1; + + taskFor(this.findNodes).perform(); + } + + @restartableTask + @waitFor + async findNodes(filter = '') { + if (filter) { + await timeout(250); + } + + const { user } = this.currentUser; + + if (!user) { + return []; + } + + // If the filter changed, reset the page number + if (filter !== this.filter) { + this.filter = filter; + this.page = 1; + } + + const more = this.page > 1; + + if (more) { + this.loadingMore = true; + } + + const nodes = await user.queryHasMany('nodes', { + filter: { + title: this.filter ? this.filter : undefined, + }, + page: this.page, + }); + + const { meta } = nodes; + this.hasMore = meta.total > meta.per_page * this.page; + const items = more ? this.items.concat(nodes) : nodes; + + this.items = items; + this.loadingMore = false; + + return items; + } + + /** + * Passed into power-select component for customized searching. + * + * @returns results if match in node, root, or parent title + */ + matcher(option: Node, searchTerm: string): -1 | 1 { + const sanitizedTerm = stripAndLower(searchTerm); + + const hasTerm = [ + option.title, + option.root && option.root.title, + option.parent && option.parent.title, + ].some(field => !!field && stripAndLower(field).includes(sanitizedTerm)); + + return hasTerm ? 1 : -1; + } + + @action + valueChanged(value?: Node): void { + if (value) { + this.selected = value; + this.args.projectSelected(value); + } + } + + @action + loadMore(this: NodePicker): Promise { + this.page += 1; + + return taskFor(this.findNodes).perform(); + } + + @action + oninput(this: NodePicker, term: string): true | Promise { + return !!term || taskFor(this.findNodes).perform(); + } +} diff --git a/lib/osf-components/addon/components/node-picker/load-more-component/component.ts b/lib/osf-components/addon/components/node-picker/load-more-component/component.ts new file mode 100644 index 00000000000..f88fbe98da5 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/load-more-component/component.ts @@ -0,0 +1,13 @@ +import Component from '@glimmer/component'; + +/** + * The Load More Node Args + */ +interface LoadMoreNodeArgs { + isLoading: boolean; + hasMore: boolean; + loadMore: () => void; +} + +// eslint-disable-next-line ember/no-empty-glimmer-component-classes +export default class NodePickerLoadMoreComponent extends Component { } diff --git a/lib/osf-components/addon/components/node-picker/load-more-component/styles.scss b/lib/osf-components/addon/components/node-picker/load-more-component/styles.scss new file mode 100644 index 00000000000..af53d040f72 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/load-more-component/styles.scss @@ -0,0 +1,12 @@ +.nobullet { + list-style: none; +} + +.text-center { + text-align: center; +} + +.ember-power-select-option { + cursor: pointer; + padding: 0 8px; +} diff --git a/lib/osf-components/addon/components/node-picker/load-more-component/template.hbs b/lib/osf-components/addon/components/node-picker/load-more-component/template.hbs new file mode 100644 index 00000000000..c1c0b90b004 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/load-more-component/template.hbs @@ -0,0 +1,12 @@ +{{#if @isLoading}} + {{t 'node.projects.load-more.loading'}} +{{else if @hasMore}} +
  • + {{t 'node.projects.load-more.load-more'}} +
  • +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/node-picker/styles.scss b/lib/osf-components/addon/components/node-picker/styles.scss new file mode 100644 index 00000000000..e4df17123b6 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/styles.scss @@ -0,0 +1,4 @@ +.form-group { + margin-bottom: 15px; + margin-top: 10px; +} diff --git a/lib/osf-components/addon/components/node-picker/template.hbs b/lib/osf-components/addon/components/node-picker/template.hbs new file mode 100644 index 00000000000..457c5cc8c66 --- /dev/null +++ b/lib/osf-components/addon/components/node-picker/template.hbs @@ -0,0 +1,28 @@ +{{#if this.findNodes.last}} +
    + + {{get-ancestor-descriptor item}} {{item.title}} + +
    +{{else}} + +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs b/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs index 4ac7dc2d870..46de8b7788e 100644 --- a/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs +++ b/lib/osf-components/addon/components/osf-navbar/preprint-links/template.hbs @@ -20,8 +20,9 @@ {{/if}}
  • {{t 'navbar.add_a_preprint' preprintWord=(t 'documentType.preprint.singularCapitalized')}} diff --git a/lib/osf-components/addon/components/subjects/manager/component.ts b/lib/osf-components/addon/components/subjects/manager/component.ts index 7938e99ea2f..7f1ec9e08ec 100644 --- a/lib/osf-components/addon/components/subjects/manager/component.ts +++ b/lib/osf-components/addon/components/subjects/manager/component.ts @@ -59,12 +59,15 @@ export default class SubjectManagerComponent extends Component { // optional metadataChangeset?: BufferedChangeset; + onchange?: () => void; + hasSubjects?: (_: boolean) => void; // private @service intl!: Intl; @service toast!: Toast; @service store!: Store; + savedSubjectIds = new Set(); selectedSubjectIds = new Set(); @@ -118,6 +121,11 @@ export default class SubjectManagerComponent extends Component { }); this.incrementProperty('selectedSubjectsChanges'); this.incrementProperty('savedSubjectsChanges'); + this.model.set('subjects', savedSubjects); + if (this.hasSubjects) { + this.metadataChangeset?.validate('subjects'); + this.hasSubjects(savedSubjectIds.size > 0); + } } @restartableTask @@ -139,6 +147,11 @@ export default class SubjectManagerComponent extends Component { if (this.metadataChangeset) { this.metadataChangeset.validate('subjects'); } + + if (this.onchange) { + this.onchange(); + } + } catch (e) { const errorMessage = this.intl.t('registries.registration_metadata.save_subjects_error'); captureException(e, { errorMessage }); @@ -197,7 +210,7 @@ export default class SubjectManagerComponent extends Component { this.incrementProperty('selectedSubjectsChanges'); // assumes the parent is already loaded in the store, which at the moment is true - if (subject.parent) { + if (subject.parent ) { this.selectSubject(subject.parent); } } diff --git a/lib/osf-components/addon/components/tags-widget/component.ts b/lib/osf-components/addon/components/tags-widget/component.ts index 6679bb35326..2210d3e11db 100644 --- a/lib/osf-components/addon/components/tags-widget/component.ts +++ b/lib/osf-components/addon/components/tags-widget/component.ts @@ -14,6 +14,7 @@ import template from './template'; interface Taggable extends OsfModel { tags: string[]; + isTagClickable: boolean; } @layout(template, styles) @@ -26,6 +27,7 @@ export default class TagsWidget extends Component.extend({ styles }) { // optional arguments readOnly = true; autoSave = true; + isTagClickable = true; onChange?: (taggable: Taggable) => void; @attribute('data-analytics-scope') @@ -39,6 +41,7 @@ export default class TagsWidget extends Component.extend({ styles }) { assert('tags-widget: You must pass in a taggable model', Boolean(this.taggable && 'tags' in this.taggable)); } + @action _addTag(tag: string) { this.analytics.trackFromElement(this.element, { @@ -63,7 +66,9 @@ export default class TagsWidget extends Component.extend({ styles }) { @action _clickTag(tag: string): void { - this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); + if (this.isTagClickable) { + this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); + } } _onChange() { diff --git a/lib/osf-components/addon/components/tags-widget/styles.scss b/lib/osf-components/addon/components/tags-widget/styles.scss index 085a150bfca..d76dcf10361 100644 --- a/lib/osf-components/addon/components/tags-widget/styles.scss +++ b/lib/osf-components/addon/components/tags-widget/styles.scss @@ -1,14 +1,15 @@ .TagsWidget.TagsWidget { - border: 0; + border: 0 !important; padding: 0; margin: 0; + margin-top: 10px; } .TagsWidget :global(.emberTagInput-tag) { background: $color-bg-blue-light; border-radius: 0; color: $color-text-black; - cursor: pointer; + cursor: default; font-size: 13px; max-width: 100%; overflow-wrap: break-word; @@ -27,6 +28,10 @@ } } +.TagsWidget :global(.emberTagInput-tag .cursor) { + cursor: pointer; +} + .TagsWidget :global(.emberTagInput-new) { width: 100%; diff --git a/lib/osf-components/addon/components/tags-widget/template.hbs b/lib/osf-components/addon/components/tags-widget/template.hbs index 54b9e60ae9f..63475ed2e2c 100644 --- a/lib/osf-components/addon/components/tags-widget/template.hbs +++ b/lib/osf-components/addon/components/tags-widget/template.hbs @@ -1,7 +1,7 @@ {{#unless @taggable.tags.length}} - +
    {{t 'osf-components.tags-widget.no_tags'}} - +
    {{/unless}} + {{!-- template-lint-disable no-invalid-interactive --}} {{tag}} diff --git a/lib/osf-components/addon/components/validated-input/base-component.ts b/lib/osf-components/addon/components/validated-input/base-component.ts index 5a2eaa7d5d6..e6362bc99d8 100644 --- a/lib/osf-components/addon/components/validated-input/base-component.ts +++ b/lib/osf-components/addon/components/validated-input/base-component.ts @@ -45,15 +45,15 @@ export default abstract class BaseValidatedInput extends Compon @computed('errors', 'validation.options', 'isRequired') get required(): boolean { - if (!this.validation) { - return false; - } if (this.isRequired === true) { return true; } if (this.isRequired === false) { return false; } + if (!this.validation) { + return false; + } const { options } = this.validation; if (!options) { return false; diff --git a/lib/osf-components/addon/components/validated-input/text/template.hbs b/lib/osf-components/addon/components/validated-input/text/template.hbs index 5921966c4b9..f08a4c45d68 100644 --- a/lib/osf-components/addon/components/validated-input/text/template.hbs +++ b/lib/osf-components/addon/components/validated-input/text/template.hbs @@ -19,6 +19,7 @@ local-class='PrefixedInput' @type={{if this.password 'password' 'text'}} @value={{this.value}} + maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} />
  • @@ -32,6 +33,7 @@ class='form-control' @type={{if this.password 'password' 'text'}} @value={{this.value}} + maxlength={{@maxlength}} {{on 'keyup' (if @onKeyUp @onKeyUp this.noop)}} /> {{/if}} diff --git a/lib/osf-components/addon/helpers/is-mobile.ts b/lib/osf-components/addon/helpers/is-mobile.ts new file mode 100644 index 00000000000..396836dba4b --- /dev/null +++ b/lib/osf-components/addon/helpers/is-mobile.ts @@ -0,0 +1,11 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; +import Media from 'ember-responsive'; + +export default class IsMobileHelper extends Helper { + @service media!: Media; + + compute(): boolean { + return this.media.isMobile; + } +} diff --git a/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/component.js b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/component.js new file mode 100644 index 00000000000..a1b4fb46d3a --- /dev/null +++ b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/form-controls/radio-button-group/radio-button/component'; diff --git a/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/template.js b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/template.js new file mode 100644 index 00000000000..b4b62ad8c67 --- /dev/null +++ b/lib/osf-components/app/components/form-controls/radio-button-group/radio-button/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/form-controls/radio-button-group/radio-button/template'; diff --git a/lib/osf-components/app/components/node-picker/component.js b/lib/osf-components/app/components/node-picker/component.js new file mode 100644 index 00000000000..a7714506134 --- /dev/null +++ b/lib/osf-components/app/components/node-picker/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/component'; diff --git a/lib/osf-components/app/components/node-picker/load-more-component/component.js b/lib/osf-components/app/components/node-picker/load-more-component/component.js new file mode 100644 index 00000000000..5dadf5b3576 --- /dev/null +++ b/lib/osf-components/app/components/node-picker/load-more-component/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/load-more-component/component'; diff --git a/lib/osf-components/app/components/node-picker/load-more-component/template.js b/lib/osf-components/app/components/node-picker/load-more-component/template.js new file mode 100644 index 00000000000..ee6eb61cd32 --- /dev/null +++ b/lib/osf-components/app/components/node-picker/load-more-component/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/load-more-component/template'; diff --git a/lib/osf-components/app/components/node-picker/template.js b/lib/osf-components/app/components/node-picker/template.js new file mode 100644 index 00000000000..9ae85c4532a --- /dev/null +++ b/lib/osf-components/app/components/node-picker/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/node-picker/template'; diff --git a/lib/osf-components/app/helpers/is-mobile.js b/lib/osf-components/app/helpers/is-mobile.js new file mode 100644 index 00000000000..f7f42c39666 --- /dev/null +++ b/lib/osf-components/app/helpers/is-mobile.js @@ -0,0 +1 @@ +export { default } from 'osf-components/helpers/is-mobile'; diff --git a/lib/registries/addon/branded/new/template.hbs b/lib/registries/addon/branded/new/template.hbs index f9f28e63489..3a7c3cb10bf 100644 --- a/lib/registries/addon/branded/new/template.hbs +++ b/lib/registries/addon/branded/new/template.hbs @@ -124,7 +124,7 @@ data-test-start-registration-button data-analytics-name={{if this.isBasedOnProject 'Create new draft registration' 'Create new no-project draft registration'}} local-class='createDraftButton' - disabled={{this.disableCreateDraft}} + disabled={{or this.disableCreateDraft this.createNewDraftRegistration.isRunning}} @type='primary' @layout='medium' {{on 'click' (perform this.createNewDraftRegistration)}} diff --git a/mirage/config.ts b/mirage/config.ts index 32187f3d1e7..ee5a5acbb0c 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -9,7 +9,7 @@ import { createCollectionSubmission, getCollectionSubmissions } from './views/co import { createSubmissionAction } from './views/collection-submission-action'; import { searchCollections } from './views/collection-search'; import { reportDelete } from './views/comment'; -import { addContributor, createBibliographicContributor } from './views/contributor'; +import { addContributor, addPreprintContributor, createBibliographicContributor } from './views/contributor'; import { createDeveloperApp, updateDeveloperApp } from './views/developer-app'; import { createDraftRegistration } from './views/draft-registration'; import { @@ -27,7 +27,7 @@ import { postCountedUsage, getNodeAnalytics } from './views/metrics'; import { addCollectionModerator, addRegistrationModerator } from './views/moderator'; import { createNode, storageStatus } from './views/node'; import { osfNestedResource, osfResource, osfToManyRelationship } from './views/osf-resource'; -import { getProviderSubjects } from './views/provider-subjects'; +import { getPreprintProviderSubjects, getProviderSubjects } from './views/provider-subjects'; import { getSubjectsAcceptable } from './views/subjects-acceptable'; import { createRegistration, @@ -50,6 +50,7 @@ import { import { updatePassword } from './views/user-password'; import * as userSettings from './views/user-setting'; import * as wb from './views/wb'; +import { createPreprint } from './views/preprint'; const { OSF: { apiUrl, shareBaseUrl, url: osfUrl } } = config; @@ -324,11 +325,20 @@ export default function(this: Server) { path: '/providers/preprints/:parentID/subjects/highlighted/', relatedModelName: 'subject', }); + + osfNestedResource(this, 'preprint-provider', 'licensesAcceptable', { + only: ['index'], + path: '/providers/preprints/:parentID/licenses/', + relatedModelName: 'license', + }); + osfNestedResource(this, 'preprint-provider', 'preprints', { path: '/providers/preprints/:parentID/preprints/', relatedModelName: 'preprint', }); + this.get('/providers/preprints/:parentID/subjects/', getPreprintProviderSubjects); + osfNestedResource(this, 'preprint-provider', 'citationStyles', { only: ['index'], path: '/providers/preprints/:parentID/citation_styles/', @@ -340,11 +350,16 @@ export default function(this: Server) { */ osfResource(this, 'preprint'); + this.post('/preprints', createPreprint); + osfNestedResource(this, 'preprint', 'contributors', { path: '/preprints/:parentID/contributors/', defaultSortKey: 'index', - relatedModelName: 'contributor', + except: ['create'], }); + + this.post('/preprints/:preprintID/contributors/', addPreprintContributor); + osfNestedResource(this, 'preprint', 'bibliographicContributors', { path: '/preprints/:parentID/bibliographic_contributors/', defaultSortKey: 'index', @@ -355,16 +370,19 @@ export default function(this: Server) { defaultSortKey: 'index', relatedModelName: 'file', }); + + this.put('/preprints/:parentID/files/:fileProviderId/upload', uploadToRoot); // Upload to file provider + osfNestedResource(this, 'preprint', 'primaryFile', { path: '/wb/files/:fileID/', defaultSortKey: 'index', relatedModelName: 'file', }); - osfNestedResource(this, 'preprint', 'subjects', { - path: '/preprints/:parentID/subjects/', - defaultSortKey: 'index', - relatedModelName: 'subject', + + osfToManyRelationship(this, 'preprint', 'subjects', { + only: ['related', 'self', 'update'], }); + osfNestedResource(this, 'preprint', 'identifiers', { path: '/preprints/:parentID/identifiers/', defaultSortKey: 'index', diff --git a/mirage/factories/file.ts b/mirage/factories/file.ts index 05938d733d0..f84db04f1d2 100644 --- a/mirage/factories/file.ts +++ b/mirage/factories/file.ts @@ -12,7 +12,7 @@ export interface FileTraits { export interface PolymorphicTargetRelationship { id: ID; - type: 'draft-nodes' | 'nodes'; + type: 'draft-nodes' | 'nodes' | 'preprints'; } export interface MirageFile extends File { diff --git a/mirage/factories/preprint.ts b/mirage/factories/preprint.ts index 7225ad6850c..b81583a7136 100644 --- a/mirage/factories/preprint.ts +++ b/mirage/factories/preprint.ts @@ -19,6 +19,8 @@ function buildLicenseText(): string { export interface PreprintMirageModel extends PreprintModel { isPreprintDoi: boolean; addLicenseName: boolean; + nodeId: number; + licenseId: number; } export interface PreprintTraits { @@ -61,6 +63,16 @@ export default Factory.extend({ year: '2023', }, + dateLastTransitioned: null, + hasCoi: null, + conflictOfInterestStatement: null, + hasDataLinks: null, + whyNoData: null, + dataLinks: null, + preregLinks: null, + preregLinkInfo: null, + hasPreregLinks: null, + dateWithdrawn: null, doi: null, @@ -133,12 +145,15 @@ export default Factory.extend({ }, }); + const providerId = preprint.id + ':osfstorage'; + const osfstorage = server.create('file-provider', { id: providerId, target: preprint }); + preprint.update({ contributors: allContributors, bibliographicContributors: allContributors, license, subjects, - files: [file], + files: [osfstorage], primaryFile: file, node, }); diff --git a/mirage/fixtures/preprint-providers.ts b/mirage/fixtures/preprint-providers.ts index e15de80a363..0e4a7d28c63 100644 --- a/mirage/fixtures/preprint-providers.ts +++ b/mirage/fixtures/preprint-providers.ts @@ -22,6 +22,7 @@ const preprintProviders: Array> = [ footerLinks: 'fake footer links', reviewsWorkflow: PreprintProviderReviewsWorkFlow.PRE_MODERATION, allowCommenting: true, + allowSubmissions: true, }, { id: 'thesiscommons', @@ -31,6 +32,7 @@ const preprintProviders: Array> = [ assets: randomAssets(2), // eslint-disable-next-line max-len footerLinks: '

    LawArXiv: Support Contact  

    \n

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

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

    AgriXiv: Support Contact      

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

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

    AgriXiv: Support Contact      

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

    ', + allowSubmissions: true, }, { id: 'workrxiv', @@ -68,6 +73,7 @@ const preprintProviders: Array> = [ preprintWord: 'work', assets: randomAssets(6), footerLinks: 'fake footer links', + allowSubmissions: true, }, { id: 'docrxiv', @@ -76,6 +82,7 @@ const preprintProviders: Array> = [ preprintWord: 'default', assets: randomAssets(7), footerLinks: 'fake footer links', + allowSubmissions: true, }, { id: 'agrixiv', @@ -84,6 +91,7 @@ const preprintProviders: Array> = [ preprintWord: 'preprint', assets: randomAssets(8), reviewsWorkflow: PreprintProviderReviewsWorkFlow.POST_MODERATION, + allowSubmissions: true, }, { id: 'biohackrxiv', @@ -91,6 +99,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(9), + allowSubmissions: true, }, { id: 'nutrixiv', @@ -98,6 +107,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10), + allowSubmissions: true, }, { id: 'paleorxiv', @@ -105,6 +115,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'preprint', assets: randomAssets(10, false), + allowSubmissions: true, }, { id: 'sportrxiv', @@ -112,6 +123,7 @@ const preprintProviders: Array> = [ advertiseOnDiscoverPage: true, preprintWord: 'paper', assets: randomAssets(10), + allowSubmissions: true, }, ]; diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index c6e933e5702..f8166062d9d 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -14,6 +14,7 @@ import { registrationFullScenario as registrationsFullScenario } from './registr import { settingsScenario } from './settings'; import { registrationsLiteScenario } from './registrations.lite'; import { registrationsManyProjectsScenario} from './registrations.many-projects'; +import { userScenario } from './user'; const { mirageScenarios, @@ -43,6 +44,9 @@ export default function(server: Server) { ]; const currentUser = server.create('user', ...userTraits); + // Add a bunch of users + userScenario(server); + // Optional Scenarios if (mirageScenarios.includes('dashboard')) { dashboardScenario(server, currentUser); diff --git a/mirage/scenarios/preprints.ts b/mirage/scenarios/preprints.ts index 440c060be20..81f44752c18 100644 --- a/mirage/scenarios/preprints.ts +++ b/mirage/scenarios/preprints.ts @@ -1,6 +1,10 @@ import { ModelInstance, Server } from 'ember-cli-mirage'; import { Permission } from 'ember-osf-web/models/osf-model'; -import { PreprintDataLinksEnum, PreprintPreregLinksEnum } from 'ember-osf-web/models/preprint'; +import { + PreprintDataLinksEnum, + PreprintPreregLinksEnum, + PreprintPreregLinkInfoEnum, +} from 'ember-osf-web/models/preprint'; import PreprintProvider from 'ember-osf-web/models/preprint-provider'; import { ReviewsState } from 'ember-osf-web/models/provider'; @@ -54,8 +58,15 @@ function buildOSF( doi: '10.30822/artk.v1i1.79', originalPublicationDate: new Date('2016-11-30T16:00:00.000000Z'), preprintDoiCreated: new Date('2016-11-30T16:00:00.000000Z'), + customPublicationCitation: 'This is the publication Citation', hasCoi: true, + conflictOfInterestStatement: 'This is the conflict of interest statement', hasDataLinks: PreprintDataLinksEnum.NOT_APPLICABLE, + dataLinks: [ + 'http://www.datalink.com/1', + 'http://www.datalink.com/2', + 'http://www.datalink.com/3', + ], hasPreregLinks: PreprintPreregLinksEnum.NOT_APPLICABLE, }); @@ -63,7 +74,7 @@ function buildOSF( approvedAdminPreprint.update({ identifiers: [osfApprovedAdminIdentifier] }); - const notContributorPreprint = server.create('preprint', { + const notContributorPreprint = server.create('preprint', Object({ provider: osf, id: 'osf-not-contributor', title: 'Preprint RWF: Pre-moderation, Non-Admin and Rejected', @@ -74,7 +85,8 @@ function buildOSF( whyNoData: `Why No Data\n${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, whyNoPrereg: `Why No Prereg\n${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, tags: [], - }); + isPreprintDoi: false, + })); const rejectedPreprint = server.create('preprint', { provider: osf, @@ -102,6 +114,7 @@ function buildOSF( hasCoi: true, hasDataLinks: PreprintDataLinksEnum.AVAILABLE, dataLinks: ['Data link 1', 'Data link 2', 'Data link 3'], + preregLinkInfo: PreprintPreregLinkInfoEnum.PREREG_ANALYSIS, hasPreregLinks: PreprintPreregLinksEnum.AVAILABLE, preregLinks: ['Prereg link 1', 'Prereg link 2', 'Prereg link 3'], conflictOfInterestStatement: `${faker.lorem.sentence(200)}\n${faker.lorem.sentence(300)}`, @@ -226,6 +239,8 @@ function buildOSF( osf.update({ allowSubmissions: true, highlightedSubjects: subjects, + subjects, + licensesAcceptable: server.schema.licenses.all(), // currentUser, // eslint-disable-next-line max-len advisory_board: '
    \n

    Advisory Group

    \n

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

    \n
    \n
      \n
    • Devin Berg : engrXiv, University of Wisconsin-Stout
    • \n
    • Pete Binfield : PeerJ PrePrints
    • \n
    • Benjamin Brown : PsyArXiv, Georgia Gwinnett College
    • \n
    • Philip Cohen : SocArXiv, University of Maryland
    • \n
    • Kathleen Fitzpatrick : Modern Language Association
    • \n
    \n
    \n
    \n
      \n
    • John Inglis : bioRxiv, Cold Spring Harbor Laboratory Press
    • \n
    • Rebecca Kennison : K | N Consultants
    • \n
    • Kristen Ratan : CoKo Foundation
    • \n
    • Oya Rieger : Ithaka S+R
    • \n
    • Judy Ruttenberg : SHARE, Association of Research Libraries
    • \n
    \n
    \n
    ', @@ -296,6 +311,8 @@ function buildrXiv( preprintrxiv.update({ allowSubmissions: true, highlightedSubjects: subjects, + subjects, + licensesAcceptable: server.schema.licenses.all(), // eslint-disable-next-line max-len advisory_board: '
    \n

    Advisory Group

    \n

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

    \n
    \n
      \n
    • Devin Berg : engrXiv, University of Wisconsin-Stout
    • \n
    • Pete Binfield : PeerJ PrePrints
    • \n
    • Benjamin Brown : PsyArXiv, Georgia Gwinnett College
    • \n
    • Philip Cohen : SocArXiv, University of Maryland
    • \n
    • Kathleen Fitzpatrick : Modern Language Association
    • \n
    \n
    \n
    \n
      \n
    • John Inglis : bioRxiv, Cold Spring Harbor Laboratory Press
    • \n
    • Rebecca Kennison : K | N Consultants
    • \n
    • Kristen Ratan : CoKo Foundation
    • \n
    • Oya Rieger : Ithaka S+R
    • \n
    • Judy Ruttenberg : SHARE, Association of Research Libraries
    • \n
    \n
    \n
    ', footer_links: '', @@ -313,7 +330,9 @@ function buildThesisCommons( currentUser: ModelInstance, ) { const thesisCommons = server.schema.preprintProviders.find('thesiscommons') as ModelInstance; + const brand = server.create('brand', { + primaryColor: '#821e1e', secondaryColor: '#94918e', heroBackgroundImage: 'https://singlecolorimage.com/get/94918e/1000x1000', @@ -329,7 +348,9 @@ function buildThesisCommons( thesisCommons.update({ highlightedSubjects: subjects, + subjects, brand, + licensesAcceptable: server.schema.licenses.all(), moderators: [currentUserModerator], preprints, description: '

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

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

    This is the description for agrixiv!

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

    This is the description for nutrixiv!

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

    This is the description for biohackrxiv!

    ', preprints: [publicDoiPreprint], }); diff --git a/mirage/scenarios/user.ts b/mirage/scenarios/user.ts new file mode 100644 index 00000000000..4914c322d22 --- /dev/null +++ b/mirage/scenarios/user.ts @@ -0,0 +1,31 @@ +import { Server } from 'ember-cli-mirage'; + + +export function userScenario(server: Server) { + server.create('user', { + givenName: 'Tom', + familyName: 'Brady', + }); + + for(let i = 1; i < 20; i++) { + server.create('user', { + givenName: 'Tom', + familyName: `Brady - ${i}`, + }); + } + + server.create('user', { + givenName: 'Harry', + familyName: 'Bailey', + }); + + server.create('user', { + givenName: 'George', + familyName: 'Bailey', + }); + + server.create('user', { + givenName: 'Taylor', + familyName: 'Swift', + }); +} diff --git a/mirage/serializers/contributor.ts b/mirage/serializers/contributor.ts index 8cc83825bdd..a7d4e34923c 100644 --- a/mirage/serializers/contributor.ts +++ b/mirage/serializers/contributor.ts @@ -31,6 +31,17 @@ export default class ContributorSerializer extends ApplicationSerializer) { const relationships: SerializedRelationships = {}; + if (model.preprint !== null) { + const { preprint } = model; + relationships.preprint = { + links: { + related: { + href: `${apiUrl}/v2/preprints/${preprint.id}`, + meta: this.buildRelatedLinkMeta(model, 'preprint'), + }, + }, + }; + } if (model.node !== null) { const { node } = model; relationships.node = { diff --git a/mirage/serializers/file-provider.ts b/mirage/serializers/file-provider.ts index 4bfa2fa425c..fc3fa4eefb5 100644 --- a/mirage/serializers/file-provider.ts +++ b/mirage/serializers/file-provider.ts @@ -59,10 +59,20 @@ export default class FileSerializer extends ApplicationSerializer) { const pathName = pluralize(underscore(model.targetId.type)); - return { - ...super.buildNormalLinks(model), + let links = { upload: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.name}/upload`, new_folder: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.name}/upload/?kind=folder`, }; + if(pathName === 'preprints') { + links = { + upload: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.id}/upload`, + new_folder: `${apiUrl}/v2/${pathName}/${model.targetId.id}/files/${model.id}/upload/?kind=folder`, + }; + } + + return { + ...super.buildNormalLinks(model), + ...links, + }; } } diff --git a/mirage/serializers/preprint.ts b/mirage/serializers/preprint.ts index 46a883b6e18..582c0cf11a5 100644 --- a/mirage/serializers/preprint.ts +++ b/mirage/serializers/preprint.ts @@ -8,7 +8,7 @@ const { OSF: { apiUrl } } = config; export default class PreprintSerializer extends ApplicationSerializer { buildNormalLinks(model: ModelInstance) { return { - self: `${apiUrl}/v2/${model.id}/`, + self: `${apiUrl}/v2/preprints/${model.id}/`, doi: model.doi ? `https://doi.org/${model.doi}` : null, preprint_doi: model.isPreprintDoi ? `https://doi.org/10.31219/osf.io/${model.id}` : null, }; @@ -44,16 +44,20 @@ export default class PreprintSerializer extends ApplicationSerializer, @@ -42,3 +43,40 @@ export function addContributor(this: HandlerContext, schema: Schema, request: Re } return contributorCreated; } + +export function addPreprintContributor(this: HandlerContext, schema: Schema, request: Request) { + const attrs = this.normalizedRequestAttrs('contributor'); + const { preprintID } = request.params; + const preprint = schema.preprints.find(preprintID) as ModelInstance; + let contributorCreated; + + if (attrs.usersId) { + // The request comes with an id in the payload + // That means we are adding an existing OSFUser as a contributor + const user = schema.users.find(attrs.usersId); + contributorCreated = schema.contributors.create({ + id: `${preprintID}-${attrs.usersId}`, + permission: attrs.permission, + bibliographic: attrs.bibliographic, + users: user, + preprint, + }); + } else if (attrs.fullName) { + // The request comes without an id in the payload + // That means we are inviting a user as a contributor + const user = schema.users.create({ fullName: attrs.fullName }); + contributorCreated = schema.contributors.create({ + id: `${preprintID}-${user.id}`, + permission: attrs.permission, + bibliographic: attrs.bibliographic, + users: user, + preprint, + }); + } + + if (contributorCreated!.bibliographic) { + preprint.bibliographicContributors.models.pushObject(contributorCreated!); + preprint.save(); + } + return contributorCreated; +} diff --git a/mirage/views/file.ts b/mirage/views/file.ts index f36fab414cb..60ed1aa2e98 100644 --- a/mirage/views/file.ts +++ b/mirage/views/file.ts @@ -2,6 +2,7 @@ import { HandlerContext, ModelInstance, Response, Schema } from 'ember-cli-mirag import { MirageNode } from 'ember-osf-web/mirage/factories/node'; import DraftNode from 'ember-osf-web/models/draft-node'; import { FileItemKinds } from 'ember-osf-web/models/base-file-item'; +import PreprintModel from 'ember-osf-web/models/preprint'; import faker from 'faker'; import { guid } from '../factories/utils'; @@ -46,9 +47,14 @@ export function uploadToRoot(this: HandlerContext, schema: Schema) { const uploadAttrs = this.request.requestBody; const { parentID, fileProviderId } = this.request.params; const { name, kind } = this.request.queryParams; - let node; + let isPreprint = false; + let node: any | PreprintModel; + if (this.request.url.includes('draft_nodes')) { node = schema.draftNodes.find(parentID); + } else if (this.request.url.includes('preprints')) { + isPreprint = true; + node = schema.preprints.find(parentID) as ModelInstance; } else { node = schema.nodes.find(parentID); if (node.storage && node.storage.isOverStorageCap) { @@ -57,8 +63,16 @@ export function uploadToRoot(this: HandlerContext, schema: Schema) { }); } } - const fileProvider = schema.fileProviders.findBy({ providerId: `${node.id}:${fileProviderId}` }); + + let fileProvider; + if (isPreprint) { + fileProvider = schema.fileProviders.findBy({ id: `${fileProviderId}` }); + } else { + fileProvider = schema.fileProviders.findBy({ providerId: `${node.id}:${fileProviderId}` }); + } + const { rootFolder } = fileProvider; + const randomNum = faker.random.number(); const fileGuid = guid('file'); const id = fileGuid(randomNum); @@ -83,8 +97,21 @@ export function uploadToRoot(this: HandlerContext, schema: Schema) { uploadedFile.size = uploadAttrs.size; } - rootFolder.files.models.pushObject(uploadedFile); - rootFolder.save(); + fileProvider.files.models.pushObject(uploadedFile); + fileProvider.save(); + + /* + if (isPreprint) { + /* eslint-disable-next-line * / + // node.primaryFile = uploadedFile; + node.save(); + } + */ + + if (rootFolder) { + rootFolder.files.models.pushObject(uploadedFile); + rootFolder.save(); + } return uploadedFile; } diff --git a/mirage/views/preprint.ts b/mirage/views/preprint.ts new file mode 100644 index 00000000000..dcc8363c805 --- /dev/null +++ b/mirage/views/preprint.ts @@ -0,0 +1,83 @@ +import { HandlerContext, ModelInstance, Request, Response, Schema } from 'ember-cli-mirage'; +import { Permission } from 'ember-osf-web/models/osf-model'; +import PreprintModel from 'ember-osf-web/models/preprint'; +import faker from 'faker'; + +import { guid } from '../factories/utils'; + + +export function createPreprint(this: HandlerContext, schema: Schema) { + const now = new Date(); + const randomNum = faker.random.number(); + const preprintGuid = guid('preprint'); + const id = preprintGuid(randomNum); + + const attrs = { + ...this.normalizedRequestAttrs('preprint'), + id, + dateModified: now, + dateCreated: now, + isPublished: false, + originalPublicationDate: null, + dateLastTransitioned: null, + hasCoi: null, + conflictOfInterestStatement: null, + hasDataLinks: null, + whyNoData: null, + dataLinks: null, + preregLinks: null, + preregLinkInfo: null, + hasPreregLinks: null, + doi: null, + dateWithdrawn: null, + public: false, + citation: null, + subjects: [], + tags: [] as string[] , + currentUserPermission: [Permission.Admin, Permission.Read, Permission.Write], + }; + const preprint = schema.preprints.create(attrs) as ModelInstance; + + + const userId = schema.roots.first().currentUserId; + + if (userId) { + const currentUser = schema.users.find(userId); + const contributor = schema.contributors.create({ + preprint, + users: currentUser, + index: 0, + permission: Permission.Admin, + bibliographic: true, + }); + + preprint.bibliographicContributors.models.push(contributor); + preprint.save(); + } + + const providerId = preprint.id + ':osfstorage'; + schema.fileProviders.create({ id: providerId, target: preprint }); + preprint.save(); + + return preprint; +} + +export function updatePreprint(this: HandlerContext, schema: Schema, request: Request) { + const resource = schema.resources.find(request.params.id); + const attributes = { + ...this.normalizedRequestAttrs('resource'), + }; + if ('pid' in attributes) { + if (!attributes.pid || !attributes.pid.startsWith('10.')) { + return new Response(400, {}, { + errors: [{ + status: '400', + detail: 'invalid doi', + source: {pointer: '/data/attributes/pid'}, + }], + }); + } + } + resource.update(attributes); + return this.serialize(resource); +} diff --git a/mirage/views/provider-subjects.ts b/mirage/views/provider-subjects.ts index 1951f1b3c39..b092f045df5 100644 --- a/mirage/views/provider-subjects.ts +++ b/mirage/views/provider-subjects.ts @@ -1,5 +1,6 @@ import { HandlerContext, ModelInstance, Request, Schema } from 'ember-cli-mirage'; import Subject from 'ember-osf-web/models/subject'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; import { process } from './utils'; export function getFilterOpts( @@ -46,3 +47,38 @@ export function getProviderSubjects(this: HandlerContext, schema: Schema, reques { defaultPageSize: Number(pageSize) }, ); } + +export function getPreprintProviderSubjects(this: HandlerContext, schema: Schema, request: Request) { + const { parentID: providerId } = request.params; + const { pageSize } = request.queryParams; + const filterOpts = getFilterOpts(request.queryParams); + + const provider = schema.preprintProviders.find(providerId) as ModelInstance; + const subjects = provider.subjects.models; + let filteredSubjects: Array>; + + if (filterOpts.type === 'parent') { + if (filterOpts.value === 'null') { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => !subject.parent, + ); + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.parent && (subject.parent.id === filterOpts.value), + ); + } + } else { + filteredSubjects = subjects.filter( + (subject: ModelInstance) => subject.text.includes(filterOpts.value), + ); + } + + return process( + schema, + request, + this, + filteredSubjects.map(subject => this.serialize(subject).data), + { defaultPageSize: Number(pageSize) }, + ); +} + diff --git a/mirage/views/utils/-private.ts b/mirage/views/utils/-private.ts index efaebd54777..23a6ceff0bf 100644 --- a/mirage/views/utils/-private.ts +++ b/mirage/views/utils/-private.ts @@ -195,21 +195,24 @@ export function compareStrings( comparisonValue: any, operator: ComparisonOperators, ): boolean { + const lowerCaseActualValue = actualValue.toLowerCase(); if (comparisonValue instanceof Array) { switch (operator) { case ComparisonOperators.Eq: - return comparisonValue.some(element => actualValue.includes(element)); + return comparisonValue.some(element => lowerCaseActualValue.includes(element.toLowerCase())); case ComparisonOperators.Ne: - return comparisonValue.every(element => !actualValue.includes(element)); + return comparisonValue.every(element => !lowerCaseActualValue.includes(element.toLowerCase())); default: throw new Error(`String arrays can't be compared with "${operator}".`); } } else { + const lowerCaseComparisonlValue = comparisonValue.toLowerCase(); switch (operator) { case ComparisonOperators.Eq: - return actualValue.includes(comparisonValue); + + return lowerCaseActualValue.includes(lowerCaseComparisonlValue); case ComparisonOperators.Ne: - return !actualValue.includes(comparisonValue); + return !lowerCaseActualValue.includes(lowerCaseComparisonlValue); default: throw new Error(`Strings can't be compared with "${operator}".`); } diff --git a/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts b/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts index e1ba4f6d316..923dad6bf1b 100644 --- a/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts +++ b/tests/engines/registries/acceptance/branded/moderation/moderators-test.ts @@ -42,7 +42,7 @@ module('Registries | Acceptance | branded.moderation | moderators', hooks => { 'On the moderators page of registries reviews'); assert.dom('[data-test-moderator-row]').exists({ count: 4 }, 'There are 4 moderators shown'); assert.dom('[data-test-delete-moderator-button]') - .exists({ count: 1 }, 'Only one moderator is able to be removed'); + .exists({ count: 2 }, 'Only one moderator is able to be removed'); assert.dom('[data-test-moderator-permission-option]') .doesNotExist('Moderators are not able to edit permissions'); assert.dom(`[data-test-delete-moderator-button=${currentUser.id}]`).exists('Only able to remove self'); @@ -64,7 +64,7 @@ module('Registries | Acceptance | branded.moderation | moderators', hooks => { assert.dom('[data-test-moderator-permission-option]') .exists({ count: 4 }, 'Admins are able to edit permissions for all users'); assert.dom('[data-test-delete-moderator-button]') - .exists({ count: 4 }, 'All moderators and admins are able to be removed'); + .exists({ count: 8 }, 'All moderators and admins are able to be removed'); assert.dom('[data-test-add-moderator-button]') .exists('Button to add moderator is visible for admins'); }); diff --git a/tests/integration/components/contributors/component-test.ts b/tests/integration/components/contributors/component-test.ts index 69c1a29d837..378f0826182 100644 --- a/tests/integration/components/contributors/component-test.ts +++ b/tests/integration/components/contributors/component-test.ts @@ -69,7 +69,6 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-contributor-card]').exists(); assert.dom('[data-test-contributor-card-main]').exists(); - assert.dom('[data-test-contributor-gravatar]').exists(); assert.dom(`[data-test-contributor-link="${contributor.id}"]`) .hasText(contributor.users.get('fullName')); assert.dom(`[data-test-contributor-permission="${contributor.id}"]`) @@ -97,7 +96,6 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-contributor-card]').exists(); assert.dom('[data-test-contributor-card-main]').exists(); - assert.dom('[data-test-contributor-gravatar]').exists(); assert.dom('[data-test-contributor-link]').doesNotExist(); assert.dom('[data-test-contributor-card-main]') .containsText(unregContributor.unregisteredContributor!); @@ -172,7 +170,6 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-contributor-card]').exists(); assert.dom('[data-test-contributor-card-main]').exists(); - assert.dom('[data-test-contributor-gravatar]').exists(); assert.dom(`[data-test-contributor-link="${contributor.id}"]`) .hasText(contributor.users.fullName); assert.dom(`[data-test-contributor-permission="${contributor.id}"]`) @@ -321,7 +318,7 @@ module('Integration | Component | contributors', hooks => { assert.dom('[data-test-user-search-input]').exists('User serach button renders'); assert.dom('[data-test-add-unregistered-contributor-button]').exists('Add unregistered contrib button renders'); - assert.dom('[data-test-user-search-results]').exists('Search result continer renders'); + assert.dom('[data-test-user-search-results]').doesNotExist('Search result container does not exist'); assert.dom('[data-test-contributor-card]').doesNotExist('No contributors are on the draft'); await fillIn('[data-test-user-search-input]', 'Bae'); await click('[data-test-user-search-button]'); diff --git a/tests/integration/components/moderators/component-test.ts b/tests/integration/components/moderators/component-test.ts index a1351c879ff..d1b36697b09 100644 --- a/tests/integration/components/moderators/component-test.ts +++ b/tests/integration/components/moderators/component-test.ts @@ -83,7 +83,7 @@ module('Integration | Component | moderators', hooks => { ); assert.dom('[data-test-moderator-link]').exists({ count: 2 }); assert.dom('[data-test-permission-group]').exists({ count: 2 }); - assert.dom('[data-test-delete-moderator-button]').exists({ count: 2 }); + assert.dom('[data-test-delete-moderator-button]').exists({ count: 4 }); assert.dom(`[data-test-moderator-row="${currentUser.id}"]>div>[data-test-permission-group]`).hasText('Admin'); assert.dom(`[data-test-moderator-row="${moderator.id}"]>div>[data-test-permission-group]`).hasText('Moderator'); await clickTrigger(`[data-test-moderator-row="${moderator.id}"]`); @@ -97,7 +97,7 @@ module('Integration | Component | moderators', hooks => { await click('[data-test-confirm-delete]'); assert.dom('[data-test-moderator-link]').exists({ count: 1 }); assert.dom('[data-test-permission-group]').exists({ count: 1 }); - assert.dom('[data-test-delete-moderator-button]').exists({ count: 1 }); + assert.dom('[data-test-delete-moderator-button]').exists({ count: 2 }); }); test('can only remove self as a moderator', async function( @@ -130,7 +130,7 @@ module('Integration | Component | moderators', hooks => { assert.dom('[data-test-add-moderator-button').doesNotExist(); assert.dom('[data-test-moderator-link]').exists({ count: 2 }); assert.dom('[data-test-permission-group]').exists({ count: 2 }); - assert.dom('[data-test-delete-moderator-button]').exists({ count: 1 }); + assert.dom('[data-test-delete-moderator-button]').exists({ count: 2 }); assert.dom(`[data-test-delete-moderator-button="${admin.id}"]`).doesNotExist(); assert.dom( `[data-test-moderator-row="${currentUser.id}"]>div>[data-test-permission-group]`, diff --git a/tests/integration/helpers/is-mobile-test.ts b/tests/integration/helpers/is-mobile-test.ts new file mode 100644 index 00000000000..05cf4b34015 --- /dev/null +++ b/tests/integration/helpers/is-mobile-test.ts @@ -0,0 +1,24 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +import { setBreakpoint } from 'ember-responsive/test-support'; + +module('Integration | Helper | is-mobile', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + setBreakpoint('mobile'); + + await render(hbs` + {{!-- template-lint-disable block-indentation --}} +
    {{if (is-mobile) 'mobile' 'not-mobile'}}
    + `); + + assert.dom('[data-test-div]').hasText('mobile'); + + setBreakpoint('desktop'); + assert.dom('[data-test-div]').hasText('not-mobile'); + }); +}); diff --git a/translations/en-us.yml b/translations/en-us.yml index bd6cbdbcd47..bd9d1023537 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -24,92 +24,96 @@ documentType: pluralCapitalized: Theses singular: thesis singularCapitalized: Thesis -contact: +contact: email: support@osf.io general: - OSF: OSF - share: Share - embed: Embed - download: Download - download_url: 'Download url' - done: Done - delete: Delete - view: View - edit: Edit - cancel: Cancel add: Add - ok: OK - apply: Apply - revisions: Revisions - md5: MD5 - date: Date - sha2: SHA2 - title: Title - contributors: Contributors - modified: Modified - description: Description - create: Create and: and - or: or + api: API + apply: Apply + asc_paren: (asc) + available: 'Available' + back: Back bookmark: Bookmark - more: more - upload: Upload - rename: Rename + cancel: Cancel + caution: Caution + close: Close + component: component + contributors: Contributors copy: Copy - move: Move - name: Name - size: Size - version: Version + cos: 'Center for Open Science' + create: Create + date: Date + delete: Delete + desc_paren: (desc) + description: Description + done: Done + download: Download + download_url: 'Download url' downloads: Downloads - close: Close - back: Back - public: Public - filter: Filter - revert: Revert - save: Save + edit: Edit ellipsis: … - warning: Warning - caution: Caution - sort_asc: 'Sort ascending' - sort_desc: 'Sort descending' + embed: Embed + engineering: 'engineering' + filter: Filter + help: Help + home: Home + hosted_on_the_osf: 'Hosted on OSF' last_modified: 'Last modified' - sort: Sort - asc_paren: (asc) - desc_paren: (desc) loading: Loading... + md5: MD5 + modified: Modified + more: more + move: Move + name: Name next: next - previous: previous - help: Help - api: API - cos: 'Center for Open Science' - home: Home + newFeaturePopoverHeading: 'New feature!' + newFeaturePopoverBody: 'You can now add funder information, resource types, and more enhanced metadata to your registration.' + no: 'No' + not-applicable: 'Not Applicable' + ok: OK + or: or + options: Options + optional: Optional + optional_paren: (optional) period: . - settings: Settings + please_confirm: 'Please confirm' + presented_by_osf: 'Presented by OSF' + previous: previous project: project - component: component + public: Public + OSF: OSF + other: Other registration: registration - hosted_on_the_osf: 'Hosted on OSF' - presented_by_osf: 'Presented by OSF' - please_confirm: 'Please confirm' + rename: Rename required: Required - options: Options - optional: Optional - optional_paren: (optional) - update: Update - user: User + revert: Revert + revisions: Revisions + save: Save + science: 'science' services: collections: Collections institutions: Institutions preprints: Preprints registries: Registries - other: Other + settings: Settings + sha2: SHA2 + share: Share + sort_asc: 'Sort ascending' + sort_desc: 'Sort descending' + sort: Sort + size: Size structured_data: json_ld_retrieval_error: 'Error retrieving JSON-LD object for Google Structured Data.' tags: 'Tags' - science: 'science' - engineering: 'engineering' - newFeaturePopoverHeading: 'New feature!' - newFeaturePopoverBody: 'You can now add funder information, resource types, and more enhanced metadata to your registration.' + title: Title + update: Update + upload: Upload + user: User + version: Version + view: View + warning: Warning + yes: 'Yes' file_actions_menu: actions: '{filename} actions' @@ -579,64 +583,64 @@ tos_consent: continue: Continue failed_save: 'Unable to save Terms of Services consent.' validationErrors: - description: 'This field' - inclusion: '{description} is not included in the list.' - exclusion: '{description} is reserved.' - invalid: '{description} is invalid.' - confirmation: '{description} doesn''t match {on}.' accepted: '{description} must be accepted.' - empty: 'This field can''t be empty.' + affirm_terms: 'You must read and agree to the Terms of Use and Privacy Policy.' + after: '{description} must be after {after}.' + before: '{description} must be before {before}.' blank: 'This field can''t be blank.' - present: '{description} must be blank.' collection: '{description} must be a collection.' - singular: '{description} can''t be a collection.' - tooLong: '{description} is too long (maximum is {max} characters).' - tooShort: '{description} is too short (minimum is {min} characters).' - before: '{description} must be before {before}.' - after: '{description} must be after {after}.' - wrongDateFormat: '{description} must be in the format of {format}.' - wrongLength: '{description} is the wrong length (should be {is} characters).' - notANumber: '{description} must be a number.' - notAnInteger: '{description} must be an integer.' - greaterThan: '{description} must be greater than {gt}.' - greaterThanOrEqualTo: '{description} must be greater than or equal to {gte}.' - equalTo: '{description} must be equal to {is}.' - lessThan: '{description} must be less than {lt}.' - lessThanOrEqualTo: '{description} must be less than or equal to {lte}.' - otherThan: '{description} must be other than {value}.' - odd: '{description} must be odd.' - even: '{description} must be even.' - positive: '{description} must be positive.' + confirmation: '{description} doesn''t match {on}.' date: '{description} must be a valid date.' - onOrAfter: '{description} must be on or after {onOrAfter}.' - onOrBefore: '{description} must be on or before {onOrBefore}.' + description: 'This field' email: '{description} must be a valid email address.' - phone: '{description} must be a valid phone number.' - url: '{description} must be a valid url.' - https_url: '{description} must be a valid https url.' - email_registered: 'This email address has already been registered.' + email_duplicate: 'Duplicate email' email_invalid: 'Invalid email address. If this should not have occurred, please report this to {supportEmail}' email_match: 'Email addresses must match.' - email_duplicate: 'Duplicate email' - password_email: 'Your password cannot be the same as your email address.' - password_old: 'Your new password cannot be the same as your old password.' - password_match: 'Passwords must match.' - recaptcha: 'Please complete reCAPTCHA.' - affirm_terms: 'You must read and agree to the Terms of Use and Privacy Policy.' - min_subjects: 'You must select at least one subject.' - node_license_invalid: 'Invalid required fields for the license' - node_license_missing_fields: 'The following required {numOfFields, plural, =1 {field is} other {fields are}} missing: {missingFields}' + email_registered: 'This email address has already been registered.' + empty: 'This field can''t be empty.' + equalTo: '{description} must be equal to {is}.' + even: '{description} must be even.' + exclusion: '{description} is reserved.' + greaterThan: '{description} must be greater than {gt}.' + greaterThanOrEqualTo: '{description} must be at least {gte}.' + https_url: '{description} must be a valid https url.' + inclusion: '{description} is not included in the list.' + invalid: '{description} is invalid.' invalid_doi: 'Please use a valid DOI format (10.xxxx/xxxxx)' + lessThan: '{description} must be less than {lt}.' + lessThanOrEqualTo: '{description} must be less than or equal to {lte}.' + license_not_accepted: 'Please select a license that is accepted by this collection.' + min_subjects: 'You must select at least one subject.' + missingFileNoProject: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found.' + moderator_comment: 'Please provide feedback for your decision.' mustSelect: 'You must select a value for this field.' - mustSelectMinOne: 'You must select at least one value for this field.' mustSelectFileMinOne: 'You must select at least one file for this field.' - missingFileNoProject: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found.' - onlyProjectOrComponentFiles: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found on this {projectOrComponent}.' + mustSelectMinOne: 'You must select at least one value for this field.' new_folder_name: 'Folder name must not be blank.' - year_format: 'Please specify a valid year.' no_updated_responses: 'No changes have been made in this update.' - moderator_comment: 'Please provide feedback for your decision.' - license_not_accepted: 'Please select a license that is accepted by this collection.' + node_license_invalid: 'Invalid required fields for the license' + node_license_missing_fields: 'The following required {numOfFields, plural, =1 {field is} other {fields are}} missing: {missingFields}' + notAnInteger: '{description} must be an integer.' + notANumber: '{description} must be a number.' + odd: '{description} must be odd.' + onlyProjectOrComponentFiles: 'The {numOfFiles, plural, =1 {file} other {files}} "{missingFilesList}" cannot be found on this {projectOrComponent}.' + onOrAfter: '{description} must be on or after {onOrAfter}.' + onOrBefore: '{description} must be on or before {onOrBefore}.' + otherThan: '{description} must be other than {value}.' + password_email: 'Your password cannot be the same as your email address.' + password_match: 'Passwords must match.' + password_old: 'Your new password cannot be the same as your old password.' + phone: '{description} must be a valid phone number.' + positive: '{description} must be positive.' + present: '{description} must be blank.' + recaptcha: 'Please complete reCAPTCHA.' + singular: '{description} can''t be a collection.' + tooLong: '{description} is too long (maximum is {max} characters).' + tooShort: '{description} is too short (minimum is {min} characters).' + url: '{description} must be a valid url.' + wrongDateFormat: '{description} must be in the format of {format}.' + wrongLength: '{description} is the wrong length (should be {is} characters).' + year_format: 'Please specify a valid year format (YYYY).' validated_input_form: discard_changes: 'Discard changes' node_navbar: @@ -742,6 +746,12 @@ node: contributors: Contributors add-ons: Add-ons settings: Settings + projects: + search-placeholder: 'Find project by name' + select-placeholder: 'Click to select project' + load-more: + loading: Loading… + load-more: 'Load More Projects' registrations: new_registration_modal: title: Register @@ -1160,35 +1170,163 @@ preprints: discover: title: 'Search' title: 'Preprints' + select: + page-title: 'Select Providers' + title: 'New Preprints' + select-button: 'Select' + deselect-button: 'Deselect' + heading: 'Select a preprint service' + paragraph: 'A preprint is a version of a scholarly or scientific paper that is posted online before it has undergone formal peer review and published in a scientific journal. Learn More.' + create_button: 'Create Preprint' + submit: + title-submit: 'New {documentType}' + title-edit: 'Edit {documentType}' + step-title: + title: 'Title and Abstract' + title-input: 'Title' + abstract-input: 'Abstract' + abstract-input-error: '20 characters' + step-file: + title: 'File' + upload-title: 'Upload your {singularPreprintWord}' + uploaded-file-title: 'Attached {singularPreprintWord} file' + file-upload-label: 'Upload from your computer' + file-upload-label-one: 'Drag and drop files here to upload' + file-upload-label-two: 'or click to browse for files.' + file-select-label: 'Select from an existing OSF project' + delete-modal-title: 'Add a new {singularPreprintWord} file' + delete-warning: 'This will allow a new version of the {singularPreprintWord} file to be uploaded to the {singularPreprintWord}. The existing file will be retained as a version of the {singularPreprintWord}.' + delete-modal-button: 'Continue' + delete-modal-button-tooltip: 'Version file' + step-metadata: + title: 'Metadata' + contributors-input: 'Contributors' + license-input: 'License' + license-description: 'A license tells others how they can use your work in the future and only applies to the information and files submitted with the registration. For more information, see this help guide.' + license-placeholder: 'Select one' + license-year-input: 'Copyright Year' + license-copyright-input: 'Copyright Holders' + subjects-input: 'Subjects' + tags-input: 'Tags' + publication-doi-input: 'Publication DOI' + publication-date-input: 'Publication Date' + publication-citation-input: 'Publication Citation' + step-assertions: + title: 'Author Assertions' + conflict-of-interest-input: 'Conflict of Interest' + conflict-of-interest-description: 'The Conflict of Interest (COI) assertion is made on behalf of all the authors listed for this preprint. COIs include: financial involvement in any entity such as honoraria, grants, speaking fees, employment, consultancies, stock ownership, expert testimony, and patents or licenses. COIs can also include non-financial interests such as personal or professional relationships or pre-existing beliefs in the subject matter or materials discussed in this preprint' + conflict-of-interest-placeholder: 'Describe' + conflict-of-interest-none: 'Author asserted there is no Conflict of Interest with this preprint.' + + public-link-add-button: 'Add another' + public-link-remove-button: 'Remove link' + + public-data-input: 'Public Data' + public-data-description: 'Data refers to raw and/or processed information (quantitative or qualitative) used for the analyses, case studies, and/or descriptive interpretation in the preprint. Public data could include data posted to open-access repositories, public archival library collection, or government archive. For data that is available under limited circumstances (e.g., after signing a data sharing agreement), choose the ‘No’ option and use the comment box to explain how others could access the data.' + public-data-link-placeholder: 'Link to data' + public-data-no-placeholder: 'Describe' + public-data-na-placeholder: 'Author asserted there is no data associated with this {singularPreprintWord}.' + + public-preregistration-input: 'Public Preregistration' + public-preregistration-description: ' + A preregistration is a description of the research design and/or analysis plan that is created and registered before researchers collected data or before they have seen/interacted with preexisting data. The description should appear in a public registry (e.g., clinicaltrials.gov, OSF, AEA registry).' + public-preregistration-link-placeholder: 'Link to preregistration' + public-preregistration-no-placeholder: 'Describe' + public-preregistration-na-placeholder: 'Author asserted there is no preregistration associated with this {singularPreprintWord}.' + + public-preregistration-link-info-placeholder: 'Choose one' + public-preregistration-link-info-designs: 'Study Design' + public-preregistration-link-info-analysis: 'Analysis Plan' + public-preregistration-link-info-both: 'Both' + step-supplements: + title: 'Supplements (Optional)' + description: 'Connect an OSF project to share data, code, protocols, or other supplemental materials.' + connect-button: 'Connect an existing OSF project' + choose-project: 'Choose project' + choose-project-line-one-description: 'This will make your project public, if it is not already.' + choose-project-line-two-description: 'The projects and components for which you have admin access are listed below.' + create-title: 'Create Project' + create-project-line-one-description: 'This creates a public project for your supplemental materials.' + create-project-line-two-description: 'Upload files and manage contributors on the project.' + project-title: 'New project title for supplemental materials.' + create-button: 'Create a new OSF project' + create-project: 'Create project' + delete-modal-title: 'Disconnect supplemental material' + delete-warning: 'This will disconnect the selected project. You can select new supplemental material or re-add the same supplemental material at a later date.' + step-review: + title: 'Review' + preprint-service: '{singularPreprintWord} Service' + preprint-title: 'Title' + contributors: 'Contributors' + publication-date: 'Publication Date' + publication-doi: 'Publication DOI' + publication-citation: 'Publication Citation' + conflict-of-interest: 'Conflict of Interest' + no-conflict-of-interest: 'Author asserted no Conflict of Interest.' + public-data: 'Public Data' + public-preregistration: 'Public Preregistration' + supplement-title: 'OSF Project' + supplement-na: 'Author did not add any supplements for this {singularPreprintWord}' + data-analytics: 'Goto {statusType} tab' + status-flow: + step-title-and-abstract: 'Title and Abstract' + step-file: 'File' + step-metadata: 'Metadata' + step-author-assertions: 'Author Assertions' + step-supplements: 'Supplements' + step-review: 'Review' + action-flow: + delete: 'Delete' + delete-modal-body: 'Are you sure you want to delete the {singularPreprintWord}? This action CAN NOT be undone.' + delete-modal-title: 'Delete {singularPreprintWord}' + error: 'Error saving {singularPreprintWord}.' + error-withdrawal: 'Error withdrawing the {singularPreprintWord}.' + next: 'Next' + next-disabled-tooltip: 'Fill in "Required *" fields to continue' + no-moderation-notice: '{pluralCapitalizedPreprintWord} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {singularPreprintWord} removal and at the discretion of the moderators.
    This request will be submitted to + {supportEmail} for review and removal. If the request is approved, this {singularPreprintWord} will be replaced by a tombstone page with metadata and the reason for withdrawal. This {singularPreprintWord} will still be searchable by other users after removal.' + post-moderation-notice: '{pluralCapitalizedPreprintWord} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {singularPreprintWord} removal and at the discretion of the moderators.
    This service uses post-moderation. This request will be submitted to service moderators for review. If the request is approved, this {singularPreprintWord} will be replaced by a tombstone page with metadata and the reason for withdrawal. This {singularPreprintWord} will still be searchable by other users after removal.' + pre-moderation-notice-accepted: '{pluralCapitalizedPreprintWord} are a permanent part of the scholarly record. Withdrawal requests are subject to this service’s policy on {singularPreprintWord} removal and at the discretion of the moderators.
    This service uses pre-moderation. This request will be submitted to service moderators for review. If the request is approved, this {singularPreprintWord} will be replaced by a tombstone page with metadata and the reason for withdrawal. This {singularPreprintWord} will still be searchable by other users after removal.' + pre-moderation-notice-pending: 'Your {singularPreprintword} is still pending approval and thus private, but can be withdrawn immediately. If you wish to provide a reason for withdrawal, it will be displayed only to service moderators. Once withdrawn, your preprint will never be made public.' + save-before-exit: 'Unsaved changes present. Are you sure you want to leave this page?' + success: '{singularPreprintWord} saved.' + success-withdrawal: 'Your {singularCapitalizedPreprintWord} has been successfully withdrawn.' + submit: 'Submit' + withdraw-button: 'Withdraw' + withdrawal-input-error: '25 characters' + withdrawal-label: 'Reason for withdrawal (required):' + withdrawal-modal-title: 'Withdraw {singularPreprintWord}' + withdrawal-placeholder: 'Comment' detail: abstract: 'Abstract' article_doi: 'Peer-reviewed Publication DOI' citations: 'Citations' collapse: 'Collapse' - date_label: + date_label: created_on: 'Created' submitted_on: 'Submitted' disciplines: 'Disciplines' expand: 'Expand' - header: + header: last_edited: 'Last edited' authors_label: 'Authors' withdrawn_on: 'Withdrawn' license: 'License' none: 'None' - original_publication_date: 'Original publication date' + publication-citation: 'Publication Citation' + original_publication_date: 'Original Publication Date' orphan_preprint: 'The user has removed this file.' preprint_doi: '{documentType} DOI' preprint_pending_doi: 'DOI created after {documentType} is made public' preprint_pending_doi_moderation: 'DOI created after moderator approval' preprint_pending_doi_minted: 'DOIs are minted by a third party, and may take up to 24 hours to be registered.' private_preprint_warning: 'This {documentType} is private. Contact {supportEmail} if this is in error.' - project_button: + project_button: edit_preprint: 'Edit {documentType}' edit_resubmit_preprint: 'Edit and resubmit' see_less: 'See less' see_more: 'See more' - share: + share: download: 'Download {documentType}' downloads: 'Downloads' download_file: 'Download file' @@ -1204,7 +1342,7 @@ preprints: author-assertions: header_label: 'Author Assertions' describe: 'Describe' - available: + available: yes: 'Yes' no: 'No' available: 'Available' @@ -1235,10 +1373,10 @@ preprints: brand_name: 'OSF' loading: 'Loading...' close: 'Close' - message: + message: base: '{name} uses {reviewsWorkflow}. This {documentType}' pending_pre: 'is not publicly available or searchable until approved by a moderator.' - pending_post: 'is publicly available and searchable but is subject to removal by a moderator.' + pending_post: 'is publicly available and searchable but is subject to removal by a moderator.' accepted: 'has been accepted by a moderator and is publicly available and searchable.' rejected: 'has been rejected by a moderator and is not publicly available or searchable.' pending_withdrawal: 'This {documentType} has been requested by the authors to be withdrawn. It will still be publicly searchable until the request has been approved.' @@ -1249,7 +1387,7 @@ preprints: rejected: 'rejected' pending_withdrawal: 'pending withdrawal' withdrawal_rejected: 'withdrawal rejected' - feedback: + feedback: moderator_feedback: 'Moderator feedback' moderator: 'Moderator' base: 'This {documentType}' @@ -1277,13 +1415,13 @@ preprints: bottom: contact: 'Contact us' p1: 'Create your own branded {documentType} servers backed by the OSF.' - div: + div: line1: 'Check out the' linkText1: 'open source code' line2: 'and our' linkText2: 'public roadmap' line3: '. Input welcome!' - advisory: + advisory: heading: 'Advisory Group' paragraph: 'Our advisory group includes leaders in preprints and scholarly communication' registries: @@ -1567,12 +1705,11 @@ registries: add_new_button: 'Add new contributors' done_add_new_button: 'Done adding new contributors' results_heading: 'Results' - search_placeholder: 'Search by name or profile information' - help_text: '

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

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

    ' + search_placeholder: 'Search by name' search: 'Search' - add_unregistered_contributor: 'Add unregistered contributor' + add_unregistered_contributor: 'Add by email address' error_loading: 'Error, could not load contributors' - no_results: 'No results found' + no-results: 'No results found' add_contributor_aria: 'Add contributor' save: 'Save' clear_all: 'Clear all' @@ -2505,13 +2642,14 @@ osf-components: reorderContributor: reorderContributor: 'Reorder contributor' success: 'Contributor order updated.' - dragHandle: '⇕' noEducation: 'No education history to show' noEmployment: 'No employment history to show' addContributor: success: 'Contributor added' errorHeading: 'Error adding contributor' - currentContributors: 'Current contributors' + addContributors: 'Add Contributors' + currentContributors: 'Contributors' + permission-warning: 'Warning: Changing your permissions will prevent you from editing your draft.' email: 'Email' fullName: 'Full name' selectPermission: 'Select permission' From 5f18a04d6dfd43896966e88ebe24774a3a85ce6f Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 8 Jul 2024 13:04:20 -0400 Subject: [PATCH 018/193] Add CHANGELOG. Bump version no. --- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cd9dddac85..5ca5cf43404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [24.05.0] - 2024-07-08 +### Added +- Add subjects to project metadata editor +- Preprints to EOW phase 2 +### Removed +- Removed LawrXiv logo from OSF Preprints discover page + + ## [24.04.0] - 2024-04-30 ### Added - Misc bug and a11y fixes @@ -1980,6 +1988,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Quick Files +[24.05.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.05.0 +[24.04.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.04.0 [24.03.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.03.0 [24.02.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.02.0 [24.01.0]: https://github.com/CenterForOpenScience/ember-osf-web/releases/tag/24.01.0 diff --git a/package.json b/package.json index 7f6db4367bf..fcaf948d0ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-osf-web", - "version": "24.04.0", + "version": "24.05.0", "private": true, "description": "Ember front-end for the Open Science Framework", "homepage": "https://github.com/CenterForOpenScience/ember-osf-web#readme", From 438fee849bf3a76a0df2165535d626837111e148 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Mon, 8 Jul 2024 17:39:38 -0400 Subject: [PATCH 019/193] hotfix to filter providers by allow_submissions --- app/preprints/select/route.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/preprints/select/route.ts b/app/preprints/select/route.ts index 6942843fddc..dc214fea5a9 100644 --- a/app/preprints/select/route.ts +++ b/app/preprints/select/route.ts @@ -11,10 +11,11 @@ export default class PreprintSelectRoute extends Route { @service store!: Store; @service theme!: Theme; - async model(){ - const submissionProviders: PreprintProviderModel[] = await this.store.findAll('preprint-provider', { - reload: true, - adapterOptions: { 'filter[allowSubmissions]': 'true' }, + async model() { + const submissionProviders: PreprintProviderModel[] = await this.store.query('preprint-provider', { + filter: { + allow_submissions: true, + }, }); this.theme.set('id', config.defaultProvider); From 0156bf73f76051cb899870b5cd2c4fa64b2fed42 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Tue, 9 Jul 2024 13:17:15 -0500 Subject: [PATCH 020/193] Fixes for eng-5940 and eng-5942 --- app/models/provider.ts | 2 +- .../preprint-state-machine/component.ts | 6 +++-- .../status-flow-display/styles.scss | 8 ++++--- .../status-flow-display/template.hbs | 5 ++-- .../-components/submit/review/component.ts | 24 ++++++++++++++++++- .../-components/submit/review/template.hbs | 17 +++++++++++++ translations/en-us.yml | 16 ++++++++----- 7 files changed, 63 insertions(+), 15 deletions(-) diff --git a/app/models/provider.ts b/app/models/provider.ts index 5d9f2597d71..a5e07a6260e 100644 --- a/app/models/provider.ts +++ b/app/models/provider.ts @@ -21,7 +21,7 @@ export interface Assets { wide_white: string; } -export enum PreprintProviderReviewsWorkFlow{ +export enum PreprintProviderReviewsWorkFlow { PRE_MODERATION = 'pre-moderation', POST_MODERATION = 'post-moderation' } diff --git a/app/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts index 5307f4039c6..0efa83c457b 100644 --- a/app/preprints/-components/submit/preprint-state-machine/component.ts +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -607,10 +607,12 @@ export default class PreprintStateMachine extends Component{ @task @waitFor public async addProjectFile(file: FileModel): Promise{ - await file.copy(this.preprint, '/', 'osfstorage'); + await file.copy(this.preprint, '/', 'osfstorage', { + conflict: 'replace', + }); const theFiles = await this.preprint.files; const rootFolder = await theFiles.firstObject!.rootFolder; const primaryFile = await rootFolder!.files; - this.preprint.set('primaryFile', primaryFile.firstObject); + this.preprint.set('primaryFile', primaryFile.lastObject); } } diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss index 60fe628658c..b203917bbaa 100644 --- a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/styles.scss @@ -10,9 +10,6 @@ background-color: inherit; z-index: 1; - &.cursor { - cursor: pointer; - } &.selected { width: 193px; @@ -57,6 +54,11 @@ font-weight: bold; } + &.cursor { + cursor: pointer; + } + + .finished { color: $brand-success; } diff --git a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs index e6ea2e99ebe..8ad088e1e29 100644 --- a/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs +++ b/app/preprints/-components/submit/preprint-state-machine/status-flow/status-flow-display/template.hbs @@ -1,7 +1,6 @@ {{#if this.shouldDisplayStatusType}}
    @@ -20,7 +19,9 @@ /> {{/if}}
    -
    +
    {{#if this.isFinished}}
    {{#if this.isFileUploadDisplayed}} diff --git a/app/preprints/-components/submit/preprint-state-machine/component.ts b/app/preprints/-components/submit/preprint-state-machine/component.ts index 0efa83c457b..7122776c368 100644 --- a/app/preprints/-components/submit/preprint-state-machine/component.ts +++ b/app/preprints/-components/submit/preprint-state-machine/component.ts @@ -229,20 +229,24 @@ export default class PreprintStateMachine extends Component{ this.args.setPageDirty(); } this.isNextButtonDisabled = true; - if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.titleAndAbstract) && + + if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.titleAndAbstract) && this.titleAndAbstractValidation ) { await this.saveOnStep(); await this.preprint.files; - this.isNextButtonDisabled = !this.metadataValidation; + this.isNextButtonDisabled = !this.fileValidation; return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.file) && this.fileValidation ) { await this.saveOnStep(); - this.isNextButtonDisabled = !this.authorAssertionValidation; + this.isNextButtonDisabled = !this.metadataValidation; return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.metadata) && this.metadataValidation ) { await this.saveOnStep(); @@ -252,13 +256,15 @@ export default class PreprintStateMachine extends Component{ this.isNextButtonDisabled = !this.supplementValidation; } return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.authorAssertions) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.authorAssertions) && this.authorAssertionValidation ) { await this.saveOnStep(); this.isNextButtonDisabled = !this.supplementValidation; return; - } else if (this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.supplements) && + } else if ( + this.statusFlowIndex === this.getTypeIndex(PreprintStatusTypeEnum.supplements) && this.supplementValidation ) { await this.saveOnStep(); diff --git a/app/preprints/-components/submit/review/template.hbs b/app/preprints/-components/submit/review/template.hbs index 5c8f05d2474..b37c9e00c0e 100644 --- a/app/preprints/-components/submit/review/template.hbs +++ b/app/preprints/-components/submit/review/template.hbs @@ -16,6 +16,9 @@
    {{this.providerAgreement}}
    +
    + {{t 'preprints.submit.step-review.agreement-provider-two' htmlSafe=true}} +
    {{/if}}
    diff --git a/translations/en-us.yml b/translations/en-us.yml index 6491eb1bb8e..85ce3962b30 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1187,17 +1187,18 @@ preprints: abstract-input: 'Abstract' abstract-input-error: '20 characters' step-file: - title: 'File' - upload-title: 'Upload your {singularPreprintWord}' + delete-modal-button: 'Continue' + delete-modal-button-tooltip: 'Version file' + delete-modal-title: 'Add a new {singularPreprintWord} file' + delete-warning: 'This will allow a new version of the {singularPreprintWord} file to be uploaded to the {singularPreprintWord}. The existing file will be retained as a version of the {singularPreprintWord}.' uploaded-file-title: 'Attached {singularPreprintWord} file' + file-select-label: 'Select from an existing OSF project' file-upload-label: 'Upload from your computer' file-upload-label-one: 'Drag and drop files here to upload' file-upload-label-two: 'or click to browse for files.' - file-select-label: 'Select from an existing OSF project' - delete-modal-title: 'Add a new {singularPreprintWord} file' - delete-warning: 'This will allow a new version of the {singularPreprintWord} file to be uploaded to the {singularPreprintWord}. The existing file will be retained as a version of the {singularPreprintWord}.' - delete-modal-button: 'Continue' - delete-modal-button-tooltip: 'Version file' + project-select-explanation: 'A file from a project can only be selected once.' + title: 'File' + upload-title: 'Upload your {singularPreprintWord}' step-metadata: title: 'Metadata' contributors-input: 'Contributors' @@ -1254,8 +1255,8 @@ preprints: delete-modal-title: 'Disconnect supplemental material' delete-warning: 'This will disconnect the selected project. You can select new supplemental material or re-add the same supplemental material at a later date.' step-review: - agreement-provider: '{providerName} uses {moderationType}. If your preprint is accepted, it will be assigned a DOI and become publicly accessible via OSF. The preprint file cannot be deleted but it can be updated or modified. You can read more about - OSF preprints moderation policies on the OSF support center.' + agreement-provider: '{providerName} uses {moderationType}. If your preprint is accepted, it will be assigned a DOI and become publicly accessible via OSF. The preprint file cannot be deleted but it can be updated or modified.' + agreement-provider-two: 'You can read more about OSF preprints moderation policies on the OSF support center.

    ' agreement-title: 'Consent to publish' agreement-user: 'By submitting this preprint you confirm that all contributors agree with sharing it and that you have the right to share this preprint.' conflict-of-interest: 'Conflict of Interest' From 7b847ddb8e1f94a4a2305e91be4270551620ee80 Mon Sep 17 00:00:00 2001 From: Brian Pilati Date: Thu, 11 Jul 2024 14:31:15 -0500 Subject: [PATCH 022/193] Fixed the strings for selecting a file from a project --- .../-components/submit/file/component.ts | 10 +++++- .../-components/submit/file/template.hbs | 35 +++++++++++-------- translations/en-us.yml | 5 +-- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/app/preprints/-components/submit/file/component.ts b/app/preprints/-components/submit/file/component.ts index b6871936a8b..64ee093aaff 100644 --- a/app/preprints/-components/submit/file/component.ts +++ b/app/preprints/-components/submit/file/component.ts @@ -50,6 +50,10 @@ export default class PreprintFile extends Component{ } } + public get isSelectProjectButtonDisplayed(): boolean { + return !this.args.manager.isEditFlow; + } + public get isSelectProjectButtonDisabled(): boolean { return this.isButtonDisabled || this.isEdit; } @@ -111,9 +115,13 @@ export default class PreprintFile extends Component{ this.validate(file); } + public get getSelectExplanationText(): string { + return this.intl.t('preprints.submit.step-file.project-select-explanation', + { singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized }); + } + public get getUploadText(): string { return this.intl.t('preprints.submit.step-file.upload-title', { singularPreprintWord: this.args.manager.provider.documentType.singularCapitalized }); - } } diff --git a/app/preprints/-components/submit/file/template.hbs b/app/preprints/-components/submit/file/template.hbs index c4f97f1034e..ee0e08e2e55 100644 --- a/app/preprints/-components/submit/file/template.hbs +++ b/app/preprints/-components/submit/file/template.hbs @@ -4,6 +4,11 @@ > {{t 'preprints.submit.step-file.title'}} + {{#if this.isSelectProjectButtonDisplayed}} +

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

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

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

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

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