diff --git a/lib/osf-components/addon/components/node-card/component.ts b/lib/osf-components/addon/components/node-card/component.ts index f40759174ba..c6fdd116a95 100644 --- a/lib/osf-components/addon/components/node-card/component.ts +++ b/lib/osf-components/addon/components/node-card/component.ts @@ -3,15 +3,28 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; import { inject as service } from '@ember/service'; import config from 'ember-get-config'; +import Store from '@ember-data/store'; import { layout } from 'ember-osf-web/decorators/component'; import Node, { NodeType } from 'ember-osf-web/models/node'; -import Registration from 'ember-osf-web/models/registration'; +import Registration, { RegistrationReviewStates } from 'ember-osf-web/models/registration'; import Analytics from 'ember-osf-web/services/analytics'; import pathJoin from 'ember-osf-web/utils/path-join'; +import Toast from 'ember-toastr/services/toast'; -import styles from './styles'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { waitFor } from '@ember/test-waiters'; +import RouterService from '@ember/routing/router-service'; +import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; +import Intl from 'ember-intl/services/intl'; +import RegistrationModel from 'ember-osf-web/models/registration'; +import { taskFor } from 'ember-concurrency-ts'; +import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; +import { assert } from '@ember/debug'; import template from './template'; +import styles from './styles'; + const { OSF: { url: baseURL } } = config; @@ -19,7 +32,10 @@ const { OSF: { url: baseURL } } = config; @tagName('') export default class NodeCard extends Component { @service analytics!: Analytics; - + @service router!: RouterService; + @service store!: Store; + @service toast!: Toast; + @service intl!: Intl; // Optional parameters node?: Node | Registration; delete?: (node: Node) => void; @@ -28,9 +44,56 @@ export default class NodeCard extends Component { // Private properties searchUrl = pathJoin(baseURL, 'search'); - + @tracked latestSchemaResponse!: SchemaResponseModel; + @tracked showNewUpdateModal = false; @computed('readOnly', 'node', 'node.{nodeType,userHasWritePermission}') get showDropdown() { return !this.readOnly && this.node && this.node.nodeType === NodeType.Fork && this.node.userHasWritePermission; } + + @task + @waitFor + async getLatestRevision(registration: RegistrationModel) { + assert('getLatestRevision requires a registration', registration); + if ( + registration.reviewsState === RegistrationReviewStates.Accepted || + registration.reviewsState === RegistrationReviewStates.Embargo + ) { + try { + const revisions = await registration.queryHasMany('schemaResponses'); + if (revisions) { + this.latestSchemaResponse = revisions[0]; + } + } catch (e) { + const errorMessage = this.intl.t('node_card.schema_response_error'); + captureException(e, {errorMessage}); + this.toast.error(getApiErrorMessage(e), errorMessage); + } + } + } + + didReceiveAttrs() { + if (this.node?.isRegistration) { + taskFor(this.getLatestRevision).perform(this.node as Registration); + } + } + + get shouldShowViewChangesButton() { + if (this.node instanceof RegistrationModel) { + return this.node.revisionState === RevisionReviewStates.RevisionInProgress || + this.node.revisionState === RevisionReviewStates.RevisionPendingModeration; + } + return false; + } + + get shouldShowUpdateButton() { + if (this.node instanceof RegistrationModel) { + return this.node.revisionState === RevisionReviewStates.Approved && + ( + this.node.reviewsState === RegistrationReviewStates.Accepted || + this.node.reviewsState === RegistrationReviewStates.Embargo + ); + } + return false; + } } diff --git a/lib/osf-components/addon/components/node-card/styles.scss b/lib/osf-components/addon/components/node-card/styles.scss index c353bff123b..00f62b4a3ae 100644 --- a/lib/osf-components/addon/components/node-card/styles.scss +++ b/lib/osf-components/addon/components/node-card/styles.scss @@ -1,5 +1,5 @@ .NodeCard { - margin: 10px 0; + margin: 10px; } .NodeCard__heading { diff --git a/lib/osf-components/addon/components/node-card/template.hbs b/lib/osf-components/addon/components/node-card/template.hbs index 38fedb8a3c6..2f05c80fb61 100644 --- a/lib/osf-components/addon/components/node-card/template.hbs +++ b/lib/osf-components/addon/components/node-card/template.hbs @@ -34,6 +34,17 @@ {{#if @node.archiving}} {{t 'node_card.registration.statuses.archiving'}} | {{/if}} + + {{#if (eq @node.revisionState 'unapproved')}} + {{t 'node_card.registration.statuses.revision_unapproved'}} | + {{else if (eq @node.revisionState 'in_progress')}} + {{t 'node_card.registration.statuses.revision_in_progress'}} | + {{else if (eq @node.revisionState 'pending_moderation')}} + {{t 'node_card.registration.statuses.revision_pending_moderation'}} | + {{else if (eq @node.revisionState 'approved')}} + {{t 'node_card.registration.statuses.revision_approved'}} | + {{/if}} + {{node-card/node-icon category=@node.category}} - - - {{t 'general.delete'}} - - @@ -157,16 +159,44 @@ {{/if}} - - - +
+ + + + {{#if this.latestSchemaResponse}} + {{#if this.shouldShowViewChangesButton}} + + + + {{/if}} + {{/if}} + {{#if this.shouldShowUpdateButton}} + + {{/if}} +
{{else}} @@ -174,3 +204,10 @@ {{/if}} +{{#if @node.isRegistration}} + +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/page-renderer/component.ts b/lib/osf-components/addon/components/registries/page-renderer/component.ts index ede7ad8043f..45b483027a5 100644 --- a/lib/osf-components/addon/components/registries/page-renderer/component.ts +++ b/lib/osf-components/addon/components/registries/page-renderer/component.ts @@ -5,8 +5,8 @@ import { layout } from 'ember-osf-web/decorators/component'; import { assert } from '@ember/debug'; import DraftRegistrationModel from 'ember-osf-web/models/draft-registration'; -import RevisionModel from 'ember-osf-web/models/revision'; import { PageManager } from 'ember-osf-web/packages/registration-schema/page-manager'; +import SchemaResponseModel from 'ember-osf-web/models/schema-response'; import styles from './styles'; import template from './template'; @@ -16,7 +16,7 @@ export default class PageRenderer extends Component { // Required param pageManager!: PageManager; draftRegistration?: DraftRegistrationModel; - revision?: RevisionModel; + revision?: SchemaResponseModel; init() { super.init(); diff --git a/mirage/scenarios/registrations.ts b/mirage/scenarios/registrations.ts index 948edb4f035..7b84a20536e 100644 --- a/mirage/scenarios/registrations.ts +++ b/mirage/scenarios/registrations.ts @@ -57,13 +57,27 @@ export function registrationScenario( const currentUserWrite = server.create('registration', { id: 'writr', registrationSchema: server.schema.registrationSchemas.find('prereg_challenge'), - currentUserPermissions: [Permission.Read, Permission.Write], + reviewsState: RegistrationReviewStates.Accepted, + revisionState: RevisionReviewStates.Approved, + currentUserPermissions: [Permission.Admin], providerSpecificMetadata: [ { field_name: 'Metadata field 1', field_value: '' }, { field_name: 'Another Field', field_value: 'Value 2' }, ], }); + server.create('schema-response', { + id: 'copyEditWritr1', + revisionJustification: 'Copy Edit', + reviewsState: RevisionReviewStates.RevisionInProgress, + revisionResponses: { + q1: 'Hello', + q2: ['List of greetings'], + }, + initiatedBy: currentUser, + registration: currentUserWrite, + }); + server.create('contributor', { users: currentUser, node: currentUserWrite }); const registrationResponses = { @@ -144,7 +158,7 @@ export function registrationScenario( }, 'withContributors', 'withReviewActions'); server.create('schema-response', { - id: 'copyEdit', + id: 'copyEditSilicon', revisionJustification: 'Copy Edit', revisionResponses: { q1: 'Good Morning', @@ -159,9 +173,9 @@ export function registrationScenario( title: 'Revision Model: Contributor View (non-Admin/Mod)', registrationSchema: server.schema.registrationSchemas.find('testSchema'), provider: egap, - reviewsState: RegistrationReviewStates.Accepted, + reviewsState: RegistrationReviewStates.Withdrawn, registeredBy: currentUser, - revisionState: RevisionReviewStates.Approved, + revisionState: RevisionReviewStates.RevisionInProgress, currentUserPermissions: [Permission.Write], providerSpecificMetadata: [ { field_name: 'IP Address', field_value: '127.0.0.1' }, @@ -174,7 +188,7 @@ export function registrationScenario( }, 'withContributors', 'withReviewActions'); server.create('schema-response', { - id: 'copyEdit', + id: 'copyEditTungsten', revisionJustification: 'Copy Edit', revisionResponses: { q1: 'Good Morning', diff --git a/tests/integration/components/node-card/component-test.ts b/tests/integration/components/node-card/component-test.ts index 9ff07ebdf62..0ed140e166c 100644 --- a/tests/integration/components/node-card/component-test.ts +++ b/tests/integration/components/node-card/component-test.ts @@ -1,7 +1,8 @@ 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 { setupIntl, t, TestContext } from 'ember-intl/test-support'; +import { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; import { setupRenderingTest } from 'ember-qunit'; import moment from 'moment'; import { module, test } from 'qunit'; @@ -23,6 +24,7 @@ module('Integration | Component | node-card', hooks => { const registration = server.create('registration', { tags: ['a', 'b', 'c'], description: 'Through the night', + revisionState: RevisionReviewStates.Approved, }); server.create('contributor', { node: registration, index: 0, bibliographic: true }); server.create('contributor', { node: registration, index: 1, bibliographic: true }); @@ -104,5 +106,8 @@ module('Integration | Component | node-card', hooks => { assert.dom(`[data-test-tags-widget-tag='${tag}']`).hasText(tag, 'Tag is correct'); } assert.dom(`[data-test-view-button='${registration.id}']`).exists('View button exists'); + + assert.dom('[data-test-update-button]').exists('Update button exists.'); + assert.dom('[data-test-update-button]').hasText(t('node_card.update_button').toString()); }); }); diff --git a/translations/en-us.yml b/translations/en-us.yml index cfe3b350c4f..f2daf2c2057 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -579,7 +579,11 @@ node_card: private_tooltip: 'This registration is private' timestamp_label: 'Registered:' statuses: - pending_registration_approval: 'Pending registration' + pending_registration_approval: 'Pending registration approval' + revision_pending_moderation: 'Pending moderation' + revision_in_progress: 'Revision in progress' + revision_unapproved: 'Revision unapproved' + revision_approved: 'Latest Update' pending_embargo_approval: 'Pending embargo' pending: 'Pending moderation' embargo: Embargoed @@ -596,7 +600,11 @@ node_card: last_updated: 'Last updated:' registry: 'Registry:' options: Options - view: 'View' + view_button: 'View' + update_button: 'Update' + view_changes_button: 'View changes' + schema_response_error: 'Updates irretreivable.' + forks: fork: Fork title: Forks @@ -1206,6 +1214,8 @@ registries: warning: 'This will cancel any approvals from other admin contributors and return the registration back to its draft form for additional changes.' reason: 'What additional changes need to be made and why?' submit: 'Submit' + + index: lead: 'The open registries network' see_example: 'See an example' @@ -1589,7 +1599,7 @@ registries: modalBodyFirst: 'Updates to registration responses can be updated by clicking “Next”.
Edits to metadata including Description, Category, License, Publication DOI, and Tags are done on the registration by clicking the ' modalBodySecond: ' icon.' modalBodyNoUpdates: 'The {registryName} does not allow updates for this registration template.
Contact their registy if you have any questions.' - learnMore: 'Read NEEDS LINK to learn more' + learnMore: 'Click here to learn more' next: Next meetings: index: