diff --git a/app/models/registration.ts b/app/models/registration.ts index 14a21eb466f..6e4fd2c5058 100644 --- a/app/models/registration.ts +++ b/app/models/registration.ts @@ -12,6 +12,7 @@ import InstitutionModel from './institution'; import NodeModel from './node'; import RegistrationProviderModel from './registration-provider'; import RegistrationSchemaModel, { RegistrationMetadata } from './registration-schema'; +import { RevisionActionTrigger } from './revision-action'; import UserModel from './user'; export enum RegistrationReviewStates { @@ -29,8 +30,11 @@ export enum RegistrationReviewStates { type NonActionableStates = RegistrationReviewStates.Initial | RegistrationReviewStates.Withdrawn | RegistrationReviewStates.Rejected; -export type ReviewsStateToDecisionMap = Exclude; -export const reviewsStateToDecisionMap: { [index in ReviewsStateToDecisionMap]: ReviewActionTrigger[] } = { +export type ReviewsStateToDecisionMap = + Exclude | RevisionReviewStates.RevisionPendingModeration; +export const reviewsStateToDecisionMap: { + [index in ReviewsStateToDecisionMap]: Array +} = { [RegistrationReviewStates.Accepted]: [ReviewActionTrigger.ForceWithdraw], [RegistrationReviewStates.Embargo]: [ReviewActionTrigger.ForceWithdraw], [RegistrationReviewStates.Pending]: @@ -39,6 +43,8 @@ export const reviewsStateToDecisionMap: { [index in ReviewsStateToDecisionMap]: [ReviewActionTrigger.AcceptWithdrawal, ReviewActionTrigger.RejectWithdrawal], [RegistrationReviewStates.PendingWithdrawRequest]: [ReviewActionTrigger.ForceWithdraw], [RegistrationReviewStates.PendingEmbargoTermination]: [ReviewActionTrigger.ForceWithdraw], + [RevisionReviewStates.RevisionPendingModeration]: + [RevisionActionTrigger.AcceptRevision, RevisionActionTrigger.RejectRevision], }; const Validations = buildValidations({ diff --git a/app/models/revision-action.ts b/app/models/revision-action.ts index ff47adf6b12..580ed3d5c18 100644 --- a/app/models/revision-action.ts +++ b/app/models/revision-action.ts @@ -1,6 +1,8 @@ import { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model'; import { computed } from '@ember/object'; -import RevisionModel from 'ember-osf-web/models/revision'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import RevisionModel, {RevisionReviewStates} from 'ember-osf-web/models/revision'; import UserModel from 'ember-osf-web/models/user'; import OsfModel from './osf-model'; @@ -13,18 +15,20 @@ export enum RevisionActionTrigger { } const TriggerToPastTenseTranslationKey: Record = { - submit_revision: 'submit', - admin_approve_revision: 'approve', - admin_reject_revision: 'reject', - accept_revision: 'accept', - reject_revision: 'reject', + submit_revision: 'registries.revisionActions.triggerPastTense.submit_revision', + admin_approve_revision: 'registries.revisionActions.triggerPastTense.admin_approve_revision', + admin_reject_revision: 'registries.revisionActions.triggerPastTense.admin_reject_revision', + accept_revision: 'registries.revisionActions.triggerPastTense.accept_revision', + reject_revision: 'registries.revisionActions.triggerPastTense.reject_revision', }; export default class RevisionActionModel extends OsfModel { + @service intl!: Intl; + @attr('string') actionTrigger!: RevisionActionTrigger; @attr('fixstring') comment!: string; - @attr('string') fromState!: string; - @attr('string') toState!: string; + @attr('string') fromState!: RevisionReviewStates; + @attr('string') toState!: RevisionReviewStates; @attr('date') dateCreated!: Date; @attr('date') dateModified!: Date; @attr('boolean') visible!: boolean; @@ -36,8 +40,9 @@ export default class RevisionActionModel extends OsfModel { target!: AsyncBelongsTo & RevisionModel; @computed('actionTrigger') - get pastTenseActionTrigger(): string { - return TriggerToPastTenseTranslationKey[this.actionTrigger] || ''; + get triggerPastTense(): string { + const key = TriggerToPastTenseTranslationKey[this.actionTrigger] || ''; + return key ? this.intl.t(key) : ''; } } diff --git a/app/models/revision.ts b/app/models/revision.ts index cd83e7d2bfa..7973146707b 100644 --- a/app/models/revision.ts +++ b/app/models/revision.ts @@ -30,7 +30,8 @@ export default class RevisionModel extends OsfModel { @belongsTo('registration') registration!: AsyncBelongsTo & RegistrationModel; @belongsTo('registration-schema') registrationSchema!: AsyncBelongsTo & RegistrationSchemaModel; - @hasMany('revision-action') actions!: AsyncHasMany & RevisionActionModel; + @hasMany('revision-action', { inverse: 'target' }) + actions!: AsyncHasMany | RevisionActionModel[]; } declare module 'ember-data/types/registries/model' { diff --git a/lib/osf-components/addon/components/registries/overview-form-renderer/component.ts b/lib/osf-components/addon/components/registries/overview-form-renderer/component.ts index fdce08d8d19..b5f338ed211 100644 --- a/lib/osf-components/addon/components/registries/overview-form-renderer/component.ts +++ b/lib/osf-components/addon/components/registries/overview-form-renderer/component.ts @@ -4,11 +4,13 @@ import { tagName } from '@ember-decorators/component'; import Component from '@ember/component'; import { waitFor } from '@ember/test-waiters'; import { restartableTask } from 'ember-concurrency'; +import Toast from 'ember-toastr/services/toast'; import { layout } from 'ember-osf-web/decorators/component'; import Registration from 'ember-osf-web/models/registration'; import RevisionModel from 'ember-osf-web/models/revision'; import { getSchemaBlockGroups, SchemaBlock, SchemaBlockGroup } from 'ember-osf-web/packages/registration-schema'; +import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; import template from './template'; @@ -16,6 +18,7 @@ import template from './template'; @layout(template) export default class RegistrationFormViewSchemaBlocks extends Component { @service store!: Store; + @service toast!: Toast; // Required parameter registration?: Registration; revisionId?: string; @@ -29,21 +32,26 @@ export default class RegistrationFormViewSchemaBlocks extends Component { @restartableTask({ on: 'didReceiveAttrs' }) @waitFor async fetchSchemaBlocks() { - let revision; - if (this.revisionId) { - revision = await this.store.findRecord('revision', this.revisionId); + try { + let revision; + if (this.revisionId) { + revision = await this.store.findRecord('revision', this.revisionId); + } this.set('revision', revision); - } - if (this.registration) { - const registrationSchema = await this.registration.registrationSchema; - const responses = revision ? revision.revisionResponses : this.registration.registrationResponses; - const schemaBlocksRef = registrationSchema.hasMany('schemaBlocks'); - const schemaBlocks = schemaBlocksRef.ids().length - ? schemaBlocksRef.value() : (await registrationSchema.loadAll('schemaBlocks')); - const schemaBlockGroups = getSchemaBlockGroups(schemaBlocks as SchemaBlock[]); - this.set('schemaBlocks', schemaBlocks); - this.set('schemaBlockGroups', schemaBlockGroups); - this.set('responses', responses); + if (this.registration) { + const registrationSchema = await this.registration.registrationSchema; + const responses = revision ? revision.revisionResponses : this.registration.registrationResponses; + const schemaBlocksRef = registrationSchema.hasMany('schemaBlocks'); + const schemaBlocks = schemaBlocksRef.ids().length + ? schemaBlocksRef.value() : (await registrationSchema.loadAll('schemaBlocks')); + const schemaBlockGroups = getSchemaBlockGroups(schemaBlocks as SchemaBlock[]); + this.set('schemaBlocks', schemaBlocks); + this.set('schemaBlockGroups', schemaBlockGroups); + this.set('responses', responses); + } + } catch (e) { + captureException(e); + this.toast.error(getApiErrorMessage(e)); } } } 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 3eba8ac2395..5b0c558f9cb 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 @@ -1,23 +1,74 @@ +import { A } from '@ember/array'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import Toast from 'ember-toastr/services/toast'; + import RegistrationModel, { RegistrationReviewStates } from 'ember-osf-web/models/registration'; +import RevisionModel, { RevisionReviewStates } from 'ember-osf-web/models/revision'; +import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; -const iconMap: Partial> = { +const iconMap: Partial> = { [RegistrationReviewStates.Pending]: 'hourglass', [RegistrationReviewStates.Withdrawn]: 'ban', [RegistrationReviewStates.Accepted]: 'check', [RegistrationReviewStates.Rejected]: 'times', [RegistrationReviewStates.PendingWithdraw]: 'clock', [RegistrationReviewStates.Embargo]: 'lock', + [RevisionReviewStates.RevisionPendingModeration]: 'hourglass', }; interface Args { registration: RegistrationModel; - state: RegistrationReviewStates; + state: RegistrationReviewStates | RevisionReviewStates.RevisionPendingModeration; } export default class RegistrationListCard extends Component { + @service toast!: Toast; + latestRevision?: RevisionModel; + + get revisionIsPending() { + return this.args.registration.revisionState === RevisionReviewStates.RevisionPendingModeration; + } + get icon(): string { const { state } = this.args; return iconMap[state] || ''; } + + get queryParams() { + const defaultQueryParams = { + mode: 'moderator', + }; + if (this.args.state === RevisionReviewStates.RevisionPendingModeration && + this.revisionIsPending && this.latestRevision) { + return { + revisionId: this.latestRevision.id, + ...defaultQueryParams, + }; + } + return defaultQueryParams; + } + + constructor(owner: unknown, args: Args) { + super(owner, args); + if (this.args.state === RevisionReviewStates.RevisionPendingModeration) { + taskFor(this.getLatestRevision).perform(); + } + } + + @task + @waitFor + async getLatestRevision() { + try { + const revisions = await this.args.registration.queryHasMany('revisions'); + this.latestRevision = A(revisions || []).objectAt(0); + } catch (e) { + captureException(e); + this.toast.error(getApiErrorMessage(e)); + throw e; + } + } } 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 0f7586d9a8b..64f3b213d91 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 @@ -1,32 +1,42 @@ -
- -
-

+ {{#if this.getLatestRevision.isRunning}} + {{placeholder.text lines=1}} + {{else}} +
- {{#if (eq @state 'rejected')}} - {{@registration.title}} - {{else}} - +
+

- {{@registration.title}} - - {{/if}} -

- -
-
+ {{#if (eq @state 'rejected')}} + {{@registration.title}} + {{else}} + + {{@registration.title}} + + {{/if}} +

+ {{#if (eq @state 'revision_pending_moderation')}} + + {{else}} + + {{/if}} +
+
+ {{/if}} + \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/registration-list/manager/component.ts b/lib/osf-components/addon/components/registries/registration-list/manager/component.ts index 70918c07719..5ea2b3a14a3 100644 --- a/lib/osf-components/addon/components/registries/registration-list/manager/component.ts +++ b/lib/osf-components/addon/components/registries/registration-list/manager/component.ts @@ -2,6 +2,7 @@ import Component from '@ember/component'; import { action, computed } from '@ember/object'; import { layout } from 'ember-osf-web/decorators/component'; import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; +import { RevisionReviewStates } from 'ember-osf-web/models/revision'; import template from './template'; @layout(template) @@ -13,12 +14,17 @@ export default class RegistrationListManager extends Component { @computed('state', 'sort') get filterParams() { - let filter = this.state; + const filter: Record = { review_state: this.state, revision_state: undefined }; if (this.state === RegistrationReviewStates.Embargo) { - filter = [RegistrationReviewStates.Embargo, RegistrationReviewStates.PendingEmbargoTermination].toString(); + filter.review_state = + [RegistrationReviewStates.Embargo, RegistrationReviewStates.PendingEmbargoTermination].toString(); } - const query: Record> = { - filter: { reviews_state: filter || 'pending' }, + if (this.state === RevisionReviewStates.RevisionPendingModeration) { + filter.revision_state = [RevisionReviewStates.RevisionPendingModeration].toString(); + filter.review_state = undefined; + } + const query: Record> = { + filter, sort: this.sort, }; diff --git a/lib/osf-components/addon/components/registries/review-actions-list/component.ts b/lib/osf-components/addon/components/registries/review-actions-list/component.ts index 289633853db..a3258eca442 100644 --- a/lib/osf-components/addon/components/registries/review-actions-list/component.ts +++ b/lib/osf-components/addon/components/registries/review-actions-list/component.ts @@ -12,9 +12,12 @@ import Toast from 'ember-toastr/services/toast'; import RegistrationModel from 'ember-osf-web/models/registration'; import ReviewActionModel from 'ember-osf-web/models/review-action'; import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; +import RevisionModel from 'ember-osf-web/models/revision'; +import RevisionActionModel from 'ember-osf-web/models/revision-action'; interface Args { registration: RegistrationModel; + revision: RevisionModel; } export default class ReviewActionsList extends Component { @@ -22,7 +25,7 @@ export default class ReviewActionsList extends Component { @service intl!: Intl; @tracked showFullActionList = false; - @tracked reviewActions?: ReviewActionModel[]; + @tracked reviewActions?: Array; get showOrHide() { return this.showFullActionList ? this.intl.t('registries.reviewActionsList.hide') @@ -43,7 +46,12 @@ export default class ReviewActionsList extends Component { @waitFor async fetchActions() { try { - this.reviewActions = (await this.args.registration.reviewActions) as ReviewActionModel[]; + if (this.args.registration) { + this.reviewActions = await this.args.registration.reviewActions as ReviewActionModel[]; + } + if (this.args.revision) { + this.reviewActions = await this.args.revision.actions as RevisionActionModel[]; + } } catch (e) { captureException(e); this.toast.error(getApiErrorMessage(e)); diff --git a/lib/osf-components/addon/components/registries/review-actions-list/review-action/component.ts b/lib/osf-components/addon/components/registries/review-actions-list/review-action/component.ts index b9db02ee5c9..161223d3fa7 100644 --- a/lib/osf-components/addon/components/registries/review-actions-list/review-action/component.ts +++ b/lib/osf-components/addon/components/registries/review-actions-list/review-action/component.ts @@ -4,10 +4,13 @@ import Intl from 'ember-intl/services/intl'; import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; import ReviewActionModel, { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; +import RevisionActionModel, { RevisionActionTrigger } from 'ember-osf-web/models/revision-action'; import formattedTimeSince from 'ember-osf-web/utils/formatted-time-since'; +type AllTriggerActions = RevisionActionTrigger | ReviewActionTrigger; + interface Args { - reviewAction: ReviewActionModel; + reviewAction: ReviewActionModel | RevisionActionModel; embargoEndDate: Date; } @@ -20,6 +23,22 @@ export default class ReviewAction extends Component { get translationString() { const { reviewAction } = this.args; + const registrationContributorActions = + [ + ReviewActionTrigger.RequestWithdrawal, + ReviewActionTrigger.RequestEmbargoTermination, + ] as AllTriggerActions[]; + const revisionContributorActions = + [ + RevisionActionTrigger.SubmitRevision, + RevisionActionTrigger.AdminApproveRevision, + RevisionActionTrigger.AdminRejectRevision, + ] as AllTriggerActions[]; + const revisionModeratorActions = + [ + RevisionActionTrigger.AcceptRevision, + RevisionActionTrigger.RejectRevision, + ] as AllTriggerActions[]; if (reviewAction.actionTrigger === ReviewActionTrigger.AcceptSubmission) { if (reviewAction.toState === RegistrationReviewStates.Embargo) { return this.intl.t('registries.reviewAction.acceptEmbargoSubmission', @@ -37,8 +56,7 @@ export default class ReviewAction extends Component { date: formattedTimeSince(reviewAction.dateModified), }); } - if ([ReviewActionTrigger.RequestWithdrawal, - ReviewActionTrigger.RequestEmbargoTermination].includes(reviewAction.actionTrigger)) { + if (registrationContributorActions.includes(reviewAction.actionTrigger)) { return this.intl.t('registries.reviewAction.contributorAction', { action: reviewAction.triggerPastTense, @@ -61,6 +79,22 @@ export default class ReviewAction extends Component { date: formattedTimeSince(reviewAction.dateModified), }); } + if (revisionContributorActions.includes(reviewAction.actionTrigger)) { + return this.intl.t('registries.reviewAction.revisionContributorAction', + { + action: reviewAction.triggerPastTense, + contributor: reviewAction.creator.get('fullName'), + date: formattedTimeSince(reviewAction.dateModified), + }); + } + if (revisionModeratorActions.includes(reviewAction.actionTrigger)) { + return this.intl.t('registries.reviewAction.revisionModeratorAction', + { + action: reviewAction.triggerPastTense, + moderator: reviewAction.creator.get('fullName'), + date: formattedTimeSince(reviewAction.dateModified), + }); + } return this.intl.t('registries.reviewAction.moderatorAction', { action: reviewAction.triggerPastTense, diff --git a/lib/registries/addon/branded/moderation/submissions/template.hbs b/lib/registries/addon/branded/moderation/submissions/template.hbs index 3f1e1384f5a..1739e0c74cf 100644 --- a/lib/registries/addon/branded/moderation/submissions/template.hbs +++ b/lib/registries/addon/branded/moderation/submissions/template.hbs @@ -18,6 +18,16 @@ {{t 'registries.moderation.states.pending'}} + - + {{#if @isViewingLatestRevision}} + + {{else}} + + {{/if}} {{#if this.hasModeratorActions}} - {{#each (get this.reviewsStateToDecisionMap @registration.reviewsState) as |option|}} + {{#each this.moderatorActions as |option|}}
{{#let (unique-id 'radio' option) as |uniqueId|}} {{#if this.showTopbar}}
- +
{{#if this.showMobileView}} diff --git a/lib/registries/addon/overview/-components/overview-topbar/template.hbs b/lib/registries/addon/overview/-components/overview-topbar/template.hbs index f4c516bdc1e..4cae1a5dc73 100644 --- a/lib/registries/addon/overview/-components/overview-topbar/template.hbs +++ b/lib/registries/addon/overview/-components/overview-topbar/template.hbs @@ -12,7 +12,10 @@ /> {{#if (and @isModeratorMode (not @registration.isAnonymous))}} - + {{else}}
diff --git a/mirage/factories/revision.ts b/mirage/factories/revision.ts index 6dd8ca3c7d3..6eef892bfe8 100644 --- a/mirage/factories/revision.ts +++ b/mirage/factories/revision.ts @@ -1,4 +1,4 @@ -import { Factory } from 'ember-cli-mirage'; +import { association, Factory, Trait, trait } from 'ember-cli-mirage'; import faker from 'faker'; import RevisionModel, { RevisionReviewStates } from 'ember-osf-web/models/revision'; @@ -6,8 +6,11 @@ import RevisionModel, { RevisionReviewStates } from 'ember-osf-web/models/revisi export interface MirageRevisionModel extends RevisionModel { registrationId: string; } +export interface RevisionTraits { + withRevisionActions: Trait; +} -export default Factory.extend({ +export default Factory.extend({ dateCreated() { return faker.date.past(1, new Date(2015, 0, 0)); }, @@ -23,6 +26,7 @@ export default Factory.extend({ isPendingCurrentUserApproval() { return false; }, + initiatedBy: association(), afterCreate(revision) { if (revision.registration) { @@ -30,6 +34,13 @@ export default Factory.extend({ revision.save(); } }, + + withRevisionActions: trait({ + afterCreate(revision, server) { + const revisionActions = server.createList('revision-action', 3, { target: revision }); + revision.update({ actions: revisionActions }); + }, + }), }); declare module 'ember-cli-mirage/types/registries/model' { diff --git a/mirage/views/registration.ts b/mirage/views/registration.ts index 7d450dc19e5..12ea1834a8b 100644 --- a/mirage/views/registration.ts +++ b/mirage/views/registration.ts @@ -5,7 +5,6 @@ import DraftNodeModel from 'ember-osf-web/models/draft-node'; import RegistrationModel, { RegistrationReviewStates } from 'ember-osf-web/models/registration'; import { MirageNode } from '../factories/node'; import { MirageRegistration } from '../factories/registration'; - import { guid } from '../factories/utils'; import { process } from './utils'; @@ -100,30 +99,21 @@ export function createRegistration(this: HandlerContext, schema: Schema) { } export function getProviderRegistrations(this: HandlerContext, schema: Schema, request: Request) { - let filterField: 'reviewsState' | 'revisionState'; - let filterParams: string | string[]; const { parentID: providerId } = request.params; - const { 'filter[reviews_state]': params, pageSize } = request.queryParams; - if (params) { - filterField = 'reviewsState'; - filterParams = params.split(','); - } else { - filterField = 'revisionState'; - filterParams = request.queryParams['filter[revision_state]'].split(','); - } - + const field = request.queryParams['filter[review_state]'] ? 'reviewsState' : 'revisionState'; + const filterParams = request.queryParams['filter[review_state]'] || request.queryParams['filter[revision_state]']; + const params = filterParams.split(','); + const { pageSize } = request.queryParams; const provider = schema.registrationProviders.find(providerId); const providerRegistrations = provider.registrations.models; let filteredRegistrations: Array> = []; - - for (const param of filterParams) { + for (const param of params) { filteredRegistrations = filteredRegistrations.concat( providerRegistrations.filter( - (registration: ModelInstance) => registration[filterField] === param, + (registration: ModelInstance) => registration[field] === param, ), ); } - return process( schema, request, diff --git a/tests/engines/registries/acceptance/branded/moderation/submissions-test.ts b/tests/engines/registries/acceptance/branded/moderation/submissions-test.ts index 45c561086a0..1b755950299 100644 --- a/tests/engines/registries/acceptance/branded/moderation/submissions-test.ts +++ b/tests/engines/registries/acceptance/branded/moderation/submissions-test.ts @@ -262,4 +262,6 @@ module('Registries | Acceptance | branded.moderation | submissions', hooks => { assert.ok(currentURL().includes('state=pending'), 'Invalid query param replaced with pending'); assert.dom('[data-test-is-selected="true"]').hasText('Pending', 'Pending tab selected'); }); + + // TODO: Update with new tabs }); diff --git a/tests/engines/registries/acceptance/overview/moderator-mode-test.ts b/tests/engines/registries/acceptance/overview/moderator-mode-test.ts index 759ebc7593f..152a3e55e4a 100644 --- a/tests/engines/registries/acceptance/overview/moderator-mode-test.ts +++ b/tests/engines/registries/acceptance/overview/moderator-mode-test.ts @@ -3,10 +3,13 @@ import { ModelInstance } from 'ember-cli-mirage'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupIntl, t, TestContext } from 'ember-intl/test-support'; import { Permission } from 'ember-osf-web/models/osf-model'; +import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; import RegistrationProviderModel from 'ember-osf-web/models/registration-provider'; +import { RevisionReviewStates } from 'ember-osf-web/models/revision'; import { click, visit } from 'ember-osf-web/tests/helpers'; import { setupEngineApplicationTest } from 'ember-osf-web/tests/helpers/engines'; import stripHtmlTags from 'ember-osf-web/utils/strip-html-tags'; +import { deserializeResponseKey } from 'ember-osf-web/transforms/registration-response-key'; import { percySnapshot } from 'ember-percy'; import moment from 'moment'; import { module, test } from 'qunit'; @@ -322,4 +325,92 @@ module('Registries | Acceptance | overview.moderator-mode', hooks => { await click('[data-test-moderation-dropdown-submit]'); assert.dom('[data-test-tombstone-title]').exists('Tombstone page shows'); }); + + test('Updates: pending -> accepted', async function(this: ModeratorModeTestContext, assert) { + const registration = server.create('registration', { + reviewsState: RegistrationReviewStates.Accepted, + revisionState: RevisionReviewStates.RevisionPendingModeration, + registrationSchema: server.schema.registrationSchemas.find('testSchema'), + provider: this.provider, + id: 'zip', + registrationResponses: { + 'page-one_short-text': 'alpaca', + 'page-one_multi-select': ['Crocs'], + }, + }); + + const revision = server.create('revision', { + reviewState: RevisionReviewStates.RevisionPendingModeration, + registration, + id: 'zap', + revisionResponses: { + 'page-one_short-text': 'llama', + 'page-one_multi-select': ['Crocs'], + }, + }, 'withRevisionActions'); + await visit(`/${registration.id}?mode=moderator&revisionId=${revision.id}`); + assert.dom('[data-test-version-metadata-title]').hasText( + t('registries.overview.versionMetadata.title'), + 'Notification box showing update metadata shown', + ); + assert.dom(`[data-test-read-only-response=${deserializeResponseKey('page-one_short-text')}]`).hasText( + 'llama', 'Revised response is shown', + ); + await click('[data-test-moderation-dropdown-button]'); + assert.dom('[data-test-moderation-dropdown-decision-label]').exists( + { count: 2 }, + 'Two moderator actions available', + ); + assert.dom('[data-test-moderation-dropdown-decision-label="accept_revision"]').hasText( + t('registries.makeDecisionDropdown.acceptRevision'), + 'Accept update option has correct text', + ); + assert.dom('[data-test-moderation-dropdown-decision-label="reject_revision"]').hasText( + t('registries.makeDecisionDropdown.rejectRevision'), + 'Reject update option has correct text', + ); + await percySnapshot(assert); + await click('[data-test-moderation-dropdown-decision-checkbox="accept_revision"]'); + await click('[data-test-moderation-dropdown-submit]'); + assert.dom(`[data-test-read-only-response=${deserializeResponseKey('page-one_short-text')}]`).hasText( + 'llama', 'Response from the accepted update still shown', + ); + }); + + test('Updates: pending -> rejected', async function(this: ModeratorModeTestContext, assert) { + const registration = server.create('registration', { + currentUserPermissions: Object.values(Permission), + reviewsState: RegistrationReviewStates.Accepted, + revisionState: RevisionReviewStates.RevisionPendingModeration, + registrationSchema: server.schema.registrationSchemas.find('testSchema'), + provider: this.provider, + id: 'zip', + registrationResponses: { + 'page-one_short-text': 'Krobus', + 'page-one_multi-select': ['Crocs'], + }, + }); + const revision = server.create('revision', { + id: 'zap', + registration, + reviewState: RevisionReviewStates.RevisionPendingModeration, + revisionResponses: { + 'page-one_short-text': 'junimo', + 'page-one_multi-select': ['Crocs'], + }, + }); + await visit(`/${registration.id}?mode=moderator&revisionId=${revision.id}`); + assert.dom(`[data-test-read-only-response=${deserializeResponseKey('page-one_short-text')}]`).hasText( + 'junimo', 'Response from the pending update shown', + ); + await click('[data-test-moderation-dropdown-button]'); + await click('[data-test-moderation-dropdown-decision-checkbox="reject_revision"]'); + await click('[data-test-moderation-dropdown-submit]'); + assert.dom('[data-test-version-metadata-title]').doesNotExist( + 'Notification box for update metadata no longer shown', + ); + assert.dom(`[data-test-read-only-response=${deserializeResponseKey('page-one_short-text')}]`).hasText( + 'Krobus', 'Response from the registration shown', + ); + }); }); diff --git a/tests/engines/registries/integration/components/make-decision-dropdown/component-test.ts b/tests/engines/registries/integration/components/make-decision-dropdown/component-test.ts index e727eadbd59..436c6f90d26 100644 --- a/tests/engines/registries/integration/components/make-decision-dropdown/component-test.ts +++ b/tests/engines/registries/integration/components/make-decision-dropdown/component-test.ts @@ -107,4 +107,6 @@ module('Registries | Integration | Component | make-decision-dropdown', hooks => } }); } + + // TODO: add tests for revisions workflow }); diff --git a/tests/integration/components/registries/registration-list/card/component-test.ts b/tests/integration/components/registries/registration-list/card/component-test.ts index 5a53a081393..9c38579543b 100644 --- a/tests/integration/components/registries/registration-list/card/component-test.ts +++ b/tests/integration/components/registries/registration-list/card/component-test.ts @@ -4,6 +4,7 @@ import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { TestContext } from 'ember-intl/test-support'; import RegistrationModel from 'ember-osf-web/models/registration'; +import { RevisionReviewStates } from 'ember-osf-web/models/revision'; import { OsfLinkRouterStub } from 'ember-osf-web/tests/integration/helpers/osf-link-router-stub'; import { setupRenderingTest } from 'ember-qunit'; import { module, test } from 'qunit'; @@ -25,6 +26,9 @@ module('Registries | Integration | Component | registration-list-card', hooks => title: 'Test title', provider, }, 'withReviewActions'); + server.create('revision', { + registration, + }, 'withRevisionActions'); this.setProperties({ registration }); }); @@ -118,4 +122,20 @@ module('Registries | Integration | Component | registration-list-card', hooks => assert.dom('[data-test-registration-title-link]').doesNotExist(); assert.dom('[data-test-registration-list-card-title]').hasText(this.registration.title); }); + + test('it renders pending revision', async function(this: ThisTestContext, assert) { + const mirageRegistration = await this.store.findRecord('registration', this.registration.id); + mirageRegistration.revisionState = RevisionReviewStates.RevisionPendingModeration; + this.set('mirageRegistration', mirageRegistration); + await render(hbs` + `); + await a11yAudit(this.element); + assert.dom('[data-test-registration-list-card]').isVisible(); + assert.dom('[data-test-registration-list-card-icon="revision_pending_moderation"]').exists(); + assert.dom('[data-test-registration-title-link]').exists(); + assert.dom('[data-test-registration-list-card-title]').hasText(this.registration.title); + }); }); diff --git a/translations/en-us.yml b/translations/en-us.yml index 52f0d37b183..869b5fbe4e5 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -1008,6 +1008,7 @@ registries: pending: Pending withdrawn: Withdrawn pendingWithdraw: 'Pending Withdrawal' + revisionPending: 'Pending Updates' submissions: title: Submissions withdrawals: @@ -1384,6 +1385,7 @@ registries: rejected: 'No rejected registrations have been found' withdrawn: 'No withdrawn registrations have been found' pending_withdraw: 'No registrations found pending withdrawal' + revision_pending_moderation: 'No registrations found with updates pending' reviewActionsList: failedToLoadActions: 'Failed to load registration moderation history' noActionsFound: 'No moderation history found for this registration' @@ -1396,6 +1398,8 @@ registries: contributorAction: 'Registration {action} {date} by contributor {contributor}' submitActionWithoutEmbargo: 'Registration submitted {date} by contributor {contributor}' submitActionWithEmbargo: 'Registration submitted {date} by contributor {contributor} with embargo ending {embargoEndDate}' + revisionContributorAction: 'Update {action} {date} by contributor {contributor}' + revisionModeratorAction: 'Update {action} {date} by moderator {moderator}' reviewActions: triggerPastTense: submit: 'submitted' @@ -1406,6 +1410,13 @@ registries: accept_withdrawal: 'withdrawal request accepted' reject_withdrawal: 'withdrawal request rejected' request_embargo_termination: 'embargo termination requested' + revisionActions: + triggerPastTense: + submit_revision: 'submitted' + admin_approve_revision: 'approved' + admin_reject_revision: 'requested further edits' + accept_revision: 'accepted' + reject_revision: 'rejected' finalizeRegistrationModal: title: 'Almost done...' notice: @@ -1433,6 +1444,10 @@ registries: acceptWithdrawalDescription: 'Registration will be withdrawn but still have a tombstone page with a justification for withdrawal and subset of the metadata available' rejectWithdrawal: 'Reject withdrawal' rejectWithdrawalDescription: 'Registration will remain fully public' + acceptRevision: 'Accept update' + acceptRevisionDescription: 'Update will be accepted and registration will be updated' + rejectRevision: 'Reject update' + rejectRevisionDescription: 'Update will be rejected and registration will remain unchanged' submit: 'Submit' success: 'Successfully submitted moderator decision.' failure: 'Failed to submit moderator decision.'