diff --git a/app/adapters/schema-response-action.ts b/app/adapters/schema-response-action.ts new file mode 100644 index 00000000000..956e4cd897e --- /dev/null +++ b/app/adapters/schema-response-action.ts @@ -0,0 +1,11 @@ +import OsfAdapter from './osf-adapter'; + +export default class SchemaResponseActionAdapter extends OsfAdapter { + parentRelationship = 'target'; +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'schema-response-action': SchemaResponseActionAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/schema-response.ts b/app/adapters/schema-response.ts new file mode 100644 index 00000000000..920fa9c4592 --- /dev/null +++ b/app/adapters/schema-response.ts @@ -0,0 +1,10 @@ +import OsfAdapter from './osf-adapter'; + +export default class SchemaResponseAdapter extends OsfAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'schema-response': SchemaResponseAdapter; + } // eslint-disable-line semi +} diff --git a/app/models/node.ts b/app/models/node.ts index 3c41079af88..a99266c027e 100644 --- a/app/models/node.ts +++ b/app/models/node.ts @@ -218,6 +218,12 @@ export default class NodeModel extends AbstractNodeModel.extend(Validations, Col return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read); } + @computed('currentUserPermissions.length') + get currentUserIsReadOnly() { + return Array.isArray(this.currentUserPermissions) && this.currentUserPermissions.includes(Permission.Read) + && this.currentUserPermissions.length === 1; + } + /** * The type of this node. */ diff --git a/app/models/provider.ts b/app/models/provider.ts index ca9287b82ff..bc598a38ade 100644 --- a/app/models/provider.ts +++ b/app/models/provider.ts @@ -50,6 +50,7 @@ export default abstract class ProviderModel extends OsfModel { @attr('string') facebookAppId!: string; @attr('boolean') allowSubmissions!: boolean; @attr('boolean') allowCommenting!: boolean; + @attr('boolean') allowUpdates!: boolean; @attr('fixstring') reviewsWorkflow!: string | null; @attr('boolean') reviewsCommentsAnonymous!: boolean | null; @attr() assets?: Partial; // TODO: camelize in transform diff --git a/app/models/registration.ts b/app/models/registration.ts index 94247c50f1d..85580f6b952 100644 --- a/app/models/registration.ts +++ b/app/models/registration.ts @@ -3,6 +3,7 @@ import { buildValidations, validator } from 'ember-cp-validations'; import DraftRegistrationModel from 'ember-osf-web/models/draft-registration'; import ReviewActionModel, { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; +import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; import { RegistrationResponse } from 'ember-osf-web/packages/registration-schema'; import CommentModel from './comment'; @@ -11,6 +12,7 @@ import InstitutionModel from './institution'; import NodeModel from './node'; import RegistrationProviderModel from './registration-provider'; import RegistrationSchemaModel, { RegistrationMetadata } from './registration-schema'; +import { SchemaResponseActionTrigger } from './schema-response-action'; import UserModel from './user'; export enum RegistrationReviewStates { @@ -25,11 +27,29 @@ export enum RegistrationReviewStates { PendingWithdraw = 'pending_withdraw', } -type NonActionableStates = RegistrationReviewStates.Initial +export type NonActionableRegistrationStates = RegistrationReviewStates.Initial | RegistrationReviewStates.Withdrawn | RegistrationReviewStates.Rejected; -export type ReviewsStateToDecisionMap = Exclude; -export const reviewsStateToDecisionMap: { [index in ReviewsStateToDecisionMap]: ReviewActionTrigger[] } = { +export type ActionableRevisionStates = RevisionReviewStates.RevisionPendingModeration; + +export type ReviewsStateToDecisionMap = + Exclude | RevisionReviewStates.RevisionPendingModeration; +export const reviewsStateToDecisionMap: { + [index in ReviewsStateToDecisionMap]: Array< + Exclude< + ReviewActionTrigger, + ReviewActionTrigger.Submit + | ReviewActionTrigger.RequestWithdrawal + | ReviewActionTrigger.RequestEmbargoTermination> + | + Exclude< + SchemaResponseActionTrigger, + SchemaResponseActionTrigger.SubmitRevision + | SchemaResponseActionTrigger.AdminApproveRevision + | SchemaResponseActionTrigger.AdminRejectRevision + > + > +} = { [RegistrationReviewStates.Accepted]: [ReviewActionTrigger.ForceWithdraw], [RegistrationReviewStates.Embargo]: [ReviewActionTrigger.ForceWithdraw], [RegistrationReviewStates.Pending]: @@ -38,6 +58,8 @@ export const reviewsStateToDecisionMap: { [index in ReviewsStateToDecisionMap]: [ReviewActionTrigger.AcceptWithdrawal, ReviewActionTrigger.RejectWithdrawal], [RegistrationReviewStates.PendingWithdrawRequest]: [ReviewActionTrigger.ForceWithdraw], [RegistrationReviewStates.PendingEmbargoTermination]: [ReviewActionTrigger.ForceWithdraw], + [RevisionReviewStates.RevisionPendingModeration]: + [SchemaResponseActionTrigger.AcceptRevision, SchemaResponseActionTrigger.RejectRevision], }; const Validations = buildValidations({ @@ -79,9 +101,10 @@ export default class RegistrationModel extends NodeModel.extend(Validations) { @attr('fixstring') articleDoi!: string | null; @attr('object') registeredMeta!: RegistrationMetadata; @attr('registration-responses') registrationResponses!: RegistrationResponse; - @attr('fixstring') reviewsState?: RegistrationReviewStates; + @attr('fixstring') reviewsState!: RegistrationReviewStates; @attr('fixstring') iaUrl?: string; @attr('array') providerSpecificMetadata!: ProviderMetadata[]; + @attr('fixstring') revisionState!: RevisionReviewStates; @attr('boolean') wikiEnabled!: boolean; // Write-only attributes @@ -121,6 +144,9 @@ export default class RegistrationModel extends NodeModel.extend(Validations) { @hasMany('review-action', { inverse: 'target' }) reviewActions!: AsyncHasMany | ReviewActionModel[]; + @hasMany('schema-response', { inverse: 'registration' }) + schemaResponses!: AsyncHasMany | SchemaResponseModel[]; + // Write-only relationships @belongsTo('draft-registration', { inverse: null }) draftRegistration!: DraftRegistrationModel; diff --git a/app/models/schema-response-action.ts b/app/models/schema-response-action.ts new file mode 100644 index 00000000000..e586e2811dc --- /dev/null +++ b/app/models/schema-response-action.ts @@ -0,0 +1,53 @@ +import { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model'; +import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; +import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; +import UserModel from 'ember-osf-web/models/user'; +import OsfModel from './osf-model'; + +export enum SchemaResponseActionTrigger { + SubmitRevision = 'submit', + AdminApproveRevision = 'approve', + AdminRejectRevision = 'admin_reject', + AcceptRevision = 'accept', + RejectRevision = 'moderator_reject', +} + +const TriggerToPastTenseTranslationKey: Record = { + submit: 'registries.schemaResponseActions.triggerPastTense.submit', + approve: 'registries.schemaResponseActions.triggerPastTense.approve', + admin_reject: 'registries.schemaResponseActions.triggerPastTense.admin_reject', + accept: 'registries.schemaResponseActions.triggerPastTense.accept', + moderator_reject: 'registries.schemaResponseActions.triggerPastTense.moderator_reject', +}; + +export default class SchemaResponseActionModel extends OsfModel { + @service intl!: Intl; + + @attr('string') actionTrigger!: SchemaResponseActionTrigger; + @attr('fixstring') comment!: string; + @attr('string') fromState!: RevisionReviewStates; + @attr('string') toState!: RevisionReviewStates; + @attr('date') dateCreated!: Date; + @attr('date') dateModified!: Date; + @attr('boolean') visible!: boolean; + + @belongsTo('user', { inverse: null }) + creator!: AsyncBelongsTo & UserModel; + + @belongsTo('schema-response', { inverse: 'actions' }) + target!: AsyncBelongsTo & SchemaResponseModel; + + @computed('actionTrigger') + get triggerPastTense(): string { + const key = TriggerToPastTenseTranslationKey[this.actionTrigger] || ''; + return key ? this.intl.t(key) : ''; + } +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'schema-response-action': SchemaResponseActionModel; + } // eslint-disable-line semi +} diff --git a/app/models/schema-response.ts b/app/models/schema-response.ts new file mode 100644 index 00000000000..1fe4436697d --- /dev/null +++ b/app/models/schema-response.ts @@ -0,0 +1,39 @@ +import { AsyncBelongsTo, AsyncHasMany, attr, belongsTo, hasMany } from '@ember-data/model'; +import RegistrationModel from 'ember-osf-web/models/registration'; +import RegistrationSchemaModel from 'ember-osf-web/models/registration-schema'; +import SchemaResponseActionModel from 'ember-osf-web/models/schema-response-action'; +import UserModel from 'ember-osf-web/models/user'; +import { RegistrationResponse } from 'ember-osf-web/packages/registration-schema'; + +import OsfModel from './osf-model'; + +export enum RevisionReviewStates { + Unapproved = 'unapproved', + RevisionInProgress = 'in_progress', + RevisionPendingModeration = 'pending_moderation', + Approved = 'approved', +} + +export default class SchemaResponseModel extends OsfModel { + @attr('fixstring') reviewsState!: RevisionReviewStates; + @attr('date') dateCreated!: Date; + @attr('date') dateModified!: Date; + @attr('fixstring') revisionJustification!: string; + @attr('registration-response-key-array') updatedResponseKeys!: string[]; + @attr('registration-responses') revisionResponses!: RegistrationResponse; + @attr('boolean') isOriginalResponse!: boolean; + @attr('boolean') isPendingCurrentUserApproval!: boolean; + + @belongsTo('user') initiatedBy!: AsyncBelongsTo & UserModel; + @belongsTo('registration') registration!: AsyncBelongsTo & RegistrationModel; + @belongsTo('registration-schema') + registrationSchema!: AsyncBelongsTo & RegistrationSchemaModel; + @hasMany('schema-response-action', { inverse: 'target' }) + actions!: AsyncHasMany | SchemaResponseActionModel[]; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'schema-response': SchemaResponseModel; + } // eslint-disable-line semi +} diff --git a/app/packages/registration-schema/get-schema-block-group.ts b/app/packages/registration-schema/get-schema-block-group.ts index 051f719312b..925171a58ea 100644 --- a/app/packages/registration-schema/get-schema-block-group.ts +++ b/app/packages/registration-schema/get-schema-block-group.ts @@ -8,7 +8,7 @@ function isEmpty(input?: string | null) { return false; } -export function getSchemaBlockGroups(blocks: SchemaBlock[] | undefined) { +export function getSchemaBlockGroups(blocks: SchemaBlock[], updatedGroupKeys?: string[]) { if (!blocks) { assert('getSchemaBlockGroups() requires blocks'); return undefined; @@ -52,6 +52,9 @@ export function getSchemaBlockGroups(blocks: SchemaBlock[] | undefined) { schemaBlockGroup.inputBlock = block; schemaBlockGroup.registrationResponseKey = block.registrationResponseKey; schemaBlockGroup.groupType = block.blockType; + if (updatedGroupKeys && updatedGroupKeys.includes(schemaBlockGroup.registrationResponseKey!)) { + schemaBlockGroup.updated = true; + } break; case 'select-input-option': if (schemaBlockGroup.inputBlock) { diff --git a/app/packages/registration-schema/index.ts b/app/packages/registration-schema/index.ts index e6cf20fd93b..887f3f23c36 100644 --- a/app/packages/registration-schema/index.ts +++ b/app/packages/registration-schema/index.ts @@ -2,7 +2,7 @@ export { getPages } from './get-pages'; export { getSchemaBlockGroups } from './get-schema-block-group'; export { SchemaBlock, SchemaBlockType } from './schema-block'; export { SchemaBlockGroup } from './schema-block-group'; -export { buildValidation, buildMetadataValidations } from './validations'; +export { buildValidation, buildMetadataValidations, buildSchemaResponseValidations } from './validations'; export { FileReference, RegistrationResponse, diff --git a/app/packages/registration-schema/schema-block-group.ts b/app/packages/registration-schema/schema-block-group.ts index 31a96ad1c0c..04dcd5893ce 100644 --- a/app/packages/registration-schema/schema-block-group.ts +++ b/app/packages/registration-schema/schema-block-group.ts @@ -9,4 +9,5 @@ export interface SchemaBlockGroup { registrationResponseKey?: string | null; groupType?: string; blocks?: SchemaBlock[]; + updated?: boolean; } diff --git a/app/packages/registration-schema/validations.ts b/app/packages/registration-schema/validations.ts index c516806b1f8..924bcfaf60d 100644 --- a/app/packages/registration-schema/validations.ts +++ b/app/packages/registration-schema/validations.ts @@ -1,7 +1,7 @@ import { assert } from '@ember/debug'; import { set } from '@ember/object'; import { ValidationObject, ValidatorFunction } from 'ember-changeset-validations'; -import { validatePresence } from 'ember-changeset-validations/validators'; +import { validateLength, validatePresence } from 'ember-changeset-validations/validators'; import DraftNode from 'ember-osf-web/models/draft-node'; import LicenseModel from 'ember-osf-web/models/license'; @@ -10,6 +10,7 @@ import NodeModel, { NodeLicense } from 'ember-osf-web/models/node'; import { RegistrationResponse } from 'ember-osf-web/packages/registration-schema'; import { SchemaBlockGroup } from 'ember-osf-web/packages/registration-schema/schema-block-group'; import { validateFileList } from 'ember-osf-web/validators/validate-response-format'; +import SchemaResponseModel from 'ember-osf-web/models/schema-response'; export const NodeLicenseFields: Record = { copyrightHolders: 'Copyright Holders', @@ -166,3 +167,22 @@ export function buildMetadataValidations() { set(validationObj, DraftMetadataProperties.NodeLicenseProperty, validateNodeLicense()); return validationObj; } + +export function buildSchemaResponseValidations() { + const validationObj: ValidationObject = {}; + const notBlank: ValidatorFunction[] = [validatePresence({ + presence: true, + ignoreBlank: true, + allowBlank: false, + allowNone: false, + type: 'blank', + })]; + set(validationObj, 'revisionJustification', notBlank); + set(validationObj, 'updatedResponseKeys', [validateLength({ + min: 1, + allowBlank: false, + allowNone: false, + type: 'no_updated_responses', + })]); + return validationObj; +} diff --git a/app/serializers/draft-registration.ts b/app/serializers/draft-registration.ts index ea215309120..ab643577481 100644 --- a/app/serializers/draft-registration.ts +++ b/app/serializers/draft-registration.ts @@ -14,7 +14,7 @@ import { mapKeysAndValues } from 'ember-osf-web/utils/map-keys'; import { Resource } from 'osf-api'; import OsfSerializer from './osf-serializer'; -interface JsonPayload { +export interface JsonPayload { attributes: Record; } @@ -60,7 +60,7 @@ export function normalizeRegistrationResponses(value: ResponseValue, store: Stor return value; } -function serializeRegistrationResponses(value: NormalizedResponseValue) { +export function serializeRegistrationResponses(value: NormalizedResponseValue) { if (Array.isArray(value) && value.length && isObject(value[0]) && 'materializedPath' in value[0]) { return value.map(file => file.toFileReference()); } diff --git a/app/serializers/schema-response-action.ts b/app/serializers/schema-response-action.ts new file mode 100644 index 00000000000..8b20c95896a --- /dev/null +++ b/app/serializers/schema-response-action.ts @@ -0,0 +1,15 @@ +import OsfSerializer from './osf-serializer'; + +export default class SchemaResponseActionSerializer extends OsfSerializer { + // Because `trigger` is a private method on DS.Model + attrs: any = { + ...this.attrs, // from OsfSerializer + actionTrigger: 'trigger', + }; +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'schema-response-action': SchemaResponseActionSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/schema-response.ts b/app/serializers/schema-response.ts new file mode 100644 index 00000000000..2070770d788 --- /dev/null +++ b/app/serializers/schema-response.ts @@ -0,0 +1,48 @@ +import Model from '@ember-data/model'; +import DS from 'ember-data'; +import { + NormalizedRegistrationResponse, + RegistrationResponse, +} from 'ember-osf-web/packages/registration-schema'; +import { mapKeysAndValues } from 'ember-osf-web/utils/map-keys'; +import { Resource } from 'osf-api'; + +import { JsonPayload, normalizeRegistrationResponses, serializeRegistrationResponses } from './draft-registration'; +import OsfSerializer from './osf-serializer'; + +export default class SchemaResponseSerializer extends OsfSerializer { + normalize(modelClass: Model, resourceHash: Resource) { + if (resourceHash.attributes) { + const revisionResponses = resourceHash.attributes.revision_responses as RegistrationResponse; + // @ts-ignore: TODO: fix types + // eslint-disable-next-line no-param-reassign + resourceHash.attributes.revision_responses = mapKeysAndValues( + revisionResponses || {}, + key => key, + value => normalizeRegistrationResponses(value, this.store), + ) as NormalizedRegistrationResponse; + } + return super.normalize(modelClass, resourceHash) as { data: Resource }; + } + + serializeAttribute(snapshot: DS.Snapshot, json: JsonPayload, key: string, attribute: object): void { + super.serializeAttribute(snapshot, json, key, attribute); + + if (key === 'revisionResponses' && json.attributes) { + const underscoreKey = this.keyForAttribute(key); + const revisionResponses = json.attributes[underscoreKey] as NormalizedRegistrationResponse; + + // eslint-disable-next-line no-param-reassign + json.attributes[underscoreKey] = mapKeysAndValues( + revisionResponses || {}, + k => k, + value => serializeRegistrationResponses(value), + ); + } + } +} +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'schema-response': SchemaResponseSerializer; + } // eslint-disable-line semi +} diff --git a/app/transforms/registration-response-key-array.ts b/app/transforms/registration-response-key-array.ts new file mode 100644 index 00000000000..a9f42dc4da9 --- /dev/null +++ b/app/transforms/registration-response-key-array.ts @@ -0,0 +1,29 @@ +import Transform from '@ember-data/serializer/transform'; +import { deserializeResponseKey, serializeResponseKey } from './registration-response-key'; + +export default class RegistrationResponseKeyArrayTransform extends Transform { + deserialize(values: string[] | null): string[] { + if (values === null) { + return []; + } + return values.map( + value => deserializeResponseKey(value), + ); + } + + serialize(values: string[] | null): string[] { + if (values === null) { + return []; + } + return values.map( + value => serializeResponseKey(value), + ); + } +} + + +declare module 'ember-data/types/registries/transform' { + export default interface TransformRegistry { + 'registration-response-key-array': {}; + } // eslint-disable-line semi +} diff --git a/lib/osf-components/addon/components/files/item/template.hbs b/lib/osf-components/addon/components/files/item/template.hbs index fe346e58d7a..2b681d966b9 100644 --- a/lib/osf-components/addon/components/files/item/template.hbs +++ b/lib/osf-components/addon/components/files/item/template.hbs @@ -11,43 +11,47 @@ {{this.item.itemName}} - - -
{{t 'osf-components.files-widget.confirm_delete.body'}}
-
-
- {{else}} -
- - {{this.item.itemName}} -
- {{#unless this.item.isFolder}} - + {{#if @filesManager.canEdit}}
{{t 'osf-components.files-widget.confirm_delete.body'}}
+ {{/if}} + {{else}} +
+ + {{this.item.itemName}} +
+ {{#unless this.item.isFolder}} + + {{#if @filesManager.canEdit}} + + +
{{t 'osf-components.files-widget.confirm_delete.body'}}
+
+
+ {{/if}} {{/unless}} {{/if}} diff --git a/lib/osf-components/addon/components/files/menu/template.hbs b/lib/osf-components/addon/components/files/menu/template.hbs index de2aec1883a..96e880e8bb3 100644 --- a/lib/osf-components/addon/components/files/menu/template.hbs +++ b/lib/osf-components/addon/components/files/menu/template.hbs @@ -3,17 +3,20 @@ @onClose={{fn this.onCloseMenu @isUploading}} as |dropdownMenu| > -
- - - -
+ {{#if this.canEdit}} +
+ + + +
+ {{/if}} 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/template.hbs b/lib/osf-components/addon/components/node-card/template.hbs index 38fedb8a3c6..0299895b154 100644 --- a/lib/osf-components/addon/components/node-card/template.hbs +++ b/lib/osf-components/addon/components/node-card/template.hbs @@ -34,6 +34,15 @@ {{#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'}} | + {{/if}} + {{node-card/node-icon category=@node.category}} {{/if}} - - - +
+ + + + {{#if this.latestSchemaResponse}} + {{#if this.shouldShowViewChangesButton}} + + + + {{/if}} + {{/if}} + {{#if this.shouldShowUpdateButton}} + + {{/if}} +
{{else}} @@ -174,3 +211,10 @@ {{/if}} +{{#if @node.isRegistration}} + +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/new-update-modal/component.ts b/lib/osf-components/addon/components/registries/new-update-modal/component.ts new file mode 100644 index 00000000000..86e887250a0 --- /dev/null +++ b/lib/osf-components/addon/components/registries/new-update-modal/component.ts @@ -0,0 +1,52 @@ +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import Store from '@ember-data/store'; +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; + +import RegistrationModel from 'ember-osf-web/models/registration'; +import SchemaResponseModel from 'ember-osf-web/models/schema-response'; +import RouterService from '@ember/routing/router-service'; +import { tracked } from '@glimmer/tracking'; + +interface Args { + registration: RegistrationModel; + isOpen: boolean; + onClose: () => void; +} + +export default class NewUpdateModal extends Component { + @service store!: Store; + @service router!: RouterService; + + @tracked showModal = false; + + get updatesAllowed(): boolean { + return this.args.registration.provider.get('allowUpdates'); + } + + get providerName(): string { + return this.args.registration.provider.get('name'); + } + + @action + showCreateModal() { + this.showModal = true; + } + + @action + closeCreateModal() { + this.showModal = false; + } + + @task + @waitFor + async createNewSchemaResponse() { + const newRevision: SchemaResponseModel = this.store.createRecord('schema-response', { + registration: this.args.registration, + }); + await newRevision.save(); + this.router.transitionTo('registries.edit-revision', newRevision.id); + } +} diff --git a/lib/osf-components/addon/components/registries/new-update-modal/template.hbs b/lib/osf-components/addon/components/registries/new-update-modal/template.hbs new file mode 100644 index 00000000000..0ed7e12764e --- /dev/null +++ b/lib/osf-components/addon/components/registries/new-update-modal/template.hbs @@ -0,0 +1,50 @@ + + + {{#if this.updatesAllowed}} + {{t 'registries.newUpdateModal.modalHeader'}} + {{else}} + {{t 'registries.newUpdateModal.modalHeaderNoUpdates'}} + {{/if}} + + + {{#if this.updatesAllowed}} + {{t 'registries.newUpdateModal.modalBodyFirst' htmlSafe=true}} + + {{t 'registries.newUpdateModal.modalBodySecond' htmlSafe=true}} + {{t 'registries.newUpdateModal.learnMore' htmlSafe=true}} + {{else}} + {{t 'registries.newUpdateModal.modalBodyNoUpdates' registryName=this.providerName htmlSafe=true}} + {{/if}} + + + {{#if this.updatesAllowed}} + + + {{else}} + + {{/if}} + + 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 6c3ef3ad50c..0779503f473 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 @@ -1,35 +1,57 @@ +import { assert } from '@ember/debug'; +import Store from '@ember-data/store'; +import { inject as service } from '@ember/service'; 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 SchemaResponseModel from 'ember-osf-web/models/schema-response'; 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'; @tagName('') @layout(template) export default class RegistrationFormViewSchemaBlocks extends Component { + @service store!: Store; + @service toast!: Toast; // Required parameter - registration?: Registration; + registration!: Registration; + revision!: SchemaResponseModel; + + // Optional parameters + updatedResponseIds?: string[]; // Private properties schemaBlocks?: SchemaBlock[]; schemaBlockGroups?: SchemaBlockGroup[]; + responses?: { [key: string]: string }; @restartableTask({ on: 'didReceiveAttrs' }) @waitFor async fetchSchemaBlocks() { - if (this.registration) { - const registrationSchema = await this.registration.registrationSchema; - 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); + try { + if (this.revision && this.registration) { + const registrationSchema = await this.registration.registrationSchema; + const responses = this.revision.revisionResponses; + const schemaBlocks: SchemaBlock[] = await registrationSchema.loadAll('schemaBlocks'); + const schemaBlockGroups = getSchemaBlockGroups(schemaBlocks, this.updatedResponseIds); + this.set('schemaBlocks', schemaBlocks); + this.set('schemaBlockGroups', schemaBlockGroups); + this.set('responses', responses); + } + } catch (e) { + captureException(e); + this.toast.error(getApiErrorMessage(e)); } } + + didReceiveAttrs() { + assert('OverviewFormRenderer needs a registration',Boolean(this.registration)); + } } diff --git a/lib/osf-components/addon/components/registries/overview-form-renderer/template.hbs b/lib/osf-components/addon/components/registries/overview-form-renderer/template.hbs index 28d3189d8b1..885cfbd9010 100644 --- a/lib/osf-components/addon/components/registries/overview-form-renderer/template.hbs +++ b/lib/osf-components/addon/components/registries/overview-form-renderer/template.hbs @@ -1,6 +1,9 @@ {{#if this.fetchSchemaBlocks.isRunning}} {{else}} + {{#if (gt this.registration.schemaResponses.length 1)}} + + {{/if}} {{/each}} 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 f65c1c66ced..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 NodeModel from 'ember-osf-web/models/node'; 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'; @@ -15,11 +15,13 @@ import template from './template'; export default class PageRenderer extends Component { // Required param pageManager!: PageManager; - node!: NodeModel; - draftRegistration!: DraftRegistrationModel; + draftRegistration?: DraftRegistrationModel; + revision?: SchemaResponseModel; init() { super.init(); - assert('A pageManger is needed for page-renderer', Boolean(this.pageManager)); + assert('A pageManager is needed for page-renderer', Boolean(this.pageManager)); + assert('A draftRegistration xor a revision is needed for page-renderer', + Boolean(this.draftRegistration) !== Boolean(this.revision)); } } diff --git a/lib/osf-components/addon/components/registries/page-renderer/template.hbs b/lib/osf-components/addon/components/registries/page-renderer/template.hbs index 30b87d27996..214d3bae3f8 100644 --- a/lib/osf-components/addon/components/registries/page-renderer/template.hbs +++ b/lib/osf-components/addon/components/registries/page-renderer/template.hbs @@ -6,6 +6,7 @@ changeset=this.pageManager.changeset onInput=@onInput draftRegistration=@draftRegistration + revision=@revision }} /> {{/each}} 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..dae5a0c76f0 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,73 @@ +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 SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; +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?: SchemaResponseModel; + + 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('schemaResponses'); + this.latestRevision = A(revisions || []).objectAt(0); + } catch (e) { + captureException(e); + this.toast.error(getApiErrorMessage(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..b8b6b93fbf0 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 (eq @state 'rejected')}} - {{@registration.title}} - {{else}} - +{{#if this.getLatestRevision.isRunning}} + + {{placeholder.text lines=1}} + +{{else}} +
+ +
+

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

+ {{#if (eq @state 'pending_moderation')}} + + {{else}} + {{/if}} -

- +
- +{{/if}} 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..8db5372635d 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/schema-response'; 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 = { reviews_state: this.state, revision_state: undefined }; if (this.state === RegistrationReviewStates.Embargo) { - filter = [RegistrationReviewStates.Embargo, RegistrationReviewStates.PendingEmbargoTermination].toString(); + filter.reviews_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.reviews_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..898040ff2c9 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 SchemaResponseModel from 'ember-osf-web/models/schema-response'; +import SchemaResponseActionModel from 'ember-osf-web/models/schema-response-action'; interface Args { registration: RegistrationModel; + revision: SchemaResponseModel; } 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 SchemaResponseActionModel[]; + } } 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..abe12a7269a 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 SchemaResponseActionModel, { SchemaResponseActionTrigger } from 'ember-osf-web/models/schema-response-action'; import formattedTimeSince from 'ember-osf-web/utils/formatted-time-since'; +type AllTriggerActions = SchemaResponseActionTrigger | ReviewActionTrigger; + interface Args { - reviewAction: ReviewActionModel; + reviewAction: ReviewActionModel | SchemaResponseActionModel; 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 = + [ + SchemaResponseActionTrigger.SubmitRevision, + SchemaResponseActionTrigger.AdminApproveRevision, + SchemaResponseActionTrigger.AdminRejectRevision, + ] as AllTriggerActions[]; + const revisionModeratorActions = + [ + SchemaResponseActionTrigger.AcceptRevision, + SchemaResponseActionTrigger.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/osf-components/addon/components/registries/review-form-renderer/template.hbs b/lib/osf-components/addon/components/registries/review-form-renderer/template.hbs index 054c5b7bd3d..77109e3c741 100644 --- a/lib/osf-components/addon/components/registries/review-form-renderer/template.hbs +++ b/lib/osf-components/addon/components/registries/review-form-renderer/template.hbs @@ -1,12 +1,13 @@ -{{assert 'Registries::ReviewFormRenderer requires a draftManager' @draftManager}} -{{#each @draftManager.pageManagers as |pageManager|}} +{{assert 'Registries::ReviewFormRenderer requires @pageManagers' @pageManagers}} +{{assert 'Registries::ReviewFormRenderer requires @responses' @responses}} +{{#each @pageManagers as |pageManager|}} {{#each pageManager.schemaBlockGroups as |group|}} { + @tracked groups?: SchemaBlockGroup[]; + + constructor(owner: unknown, args: Args) { + super(owner, args); + this.groups = getSchemaBlockGroups(this.args.schemaBlocks); + } + + get updatedResponseLabels(): string[] { + const { revision } = this.args; + if (revision.updatedResponseKeys) { + const allRevisedLabels = revision.updatedResponseKeys + .reduce((labels: string[], revisedResponse: string) => { + const revisedGroup = this.groups?.filter( + (group: SchemaBlockGroup) => group.registrationResponseKey?.includes(revisedResponse), + ); + if (revisedResponse) { + labels.push(revisedGroup![0].labelBlock!.displayText!); + } + return labels; + }, []); + return allRevisedLabels; + } + return []; + } +} diff --git a/lib/osf-components/addon/components/registries/revised-responses-list/template.hbs b/lib/osf-components/addon/components/registries/revised-responses-list/template.hbs new file mode 100644 index 00000000000..f683bf96bcb --- /dev/null +++ b/lib/osf-components/addon/components/registries/revised-responses-list/template.hbs @@ -0,0 +1,9 @@ +{{~#each this.updatedResponseLabels as |response|~}} +

+ {{~response~}} +

+{{~else}} +

+ {{~t 'registries.revisedResponsesList.noResponses'~}} +

+{{/each}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/revision-justification-renderer/label-display/styles.scss b/lib/osf-components/addon/components/registries/revision-justification-renderer/label-display/styles.scss new file mode 100644 index 00000000000..4c15b1a4be5 --- /dev/null +++ b/lib/osf-components/addon/components/registries/revision-justification-renderer/label-display/styles.scss @@ -0,0 +1,14 @@ +.ResponseValue { + composes: Element from '../../schema-block-renderer/styles.scss'; + margin: 10px 0 0; +} + +.ValidationErrors { + composes: Element from '../../schema-block-renderer/styles.scss'; +} + +.DisplayText { + font-size: 14px; + font-weight: 700; + margin: 20px 0 0; +} diff --git a/lib/osf-components/addon/components/registries/revision-justification-renderer/label-display/template.hbs b/lib/osf-components/addon/components/registries/revision-justification-renderer/label-display/template.hbs new file mode 100644 index 00000000000..0c69d73ff5c --- /dev/null +++ b/lib/osf-components/addon/components/registries/revision-justification-renderer/label-display/template.hbs @@ -0,0 +1,32 @@ +{{assert 'Registries::RevisionJustificationRenderer::LabelDisplay requires a revision' @revision}} +{{assert 'Registries::RevisionJustificationRenderer::LabelDisplay requires a field' @field}} +{{assert 'Registries::RevisionJustificationRenderer::LabelDisplay requires a changeset' @changeset}} +

+ {{t (concat 'registries.edit_revision.' @field)}} + {{#if @showEditButton}} + + + + {{/if}} +

+

+ {{~yield~}} +

+{{#unless @hideError}} + +{{/unless}} diff --git a/lib/osf-components/addon/components/registries/revision-justification-renderer/styles.scss b/lib/osf-components/addon/components/registries/revision-justification-renderer/styles.scss new file mode 100644 index 00000000000..3be03c7ab59 --- /dev/null +++ b/lib/osf-components/addon/components/registries/revision-justification-renderer/styles.scss @@ -0,0 +1,12 @@ +.NoResponse, +.RevisedResponses { + font-style: italic; +} + +.TextResponse { + white-space: pre-wrap; +} + +.ValidationErrors { + composes: Element from '../schema-block-renderer/styles.scss'; +} diff --git a/lib/osf-components/addon/components/registries/revision-justification-renderer/template.hbs b/lib/osf-components/addon/components/registries/revision-justification-renderer/template.hbs new file mode 100644 index 00000000000..7e1a93c3f2a --- /dev/null +++ b/lib/osf-components/addon/components/registries/revision-justification-renderer/template.hbs @@ -0,0 +1,25 @@ +{{assert 'Registries::RevisionJustificationRenderer requires a revision' @revision}} +

{{t 'registries.edit_revision.justification.page_label'}}

+ + + {{~if @revision.revisionJustification @revision.revisionJustification (t 'registries.edit_revision.review.no_justification')~}} + + + + + diff --git a/lib/osf-components/addon/components/registries/schema-block-group-renderer/template.hbs b/lib/osf-components/addon/components/registries/schema-block-group-renderer/template.hbs index ab80ce063a9..85e8485eec0 100644 --- a/lib/osf-components/addon/components/registries/schema-block-group-renderer/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-group-renderer/template.hbs @@ -8,6 +8,7 @@ @renderStrategy={{@renderStrategy}} @isRequired={{this.isRequired}} @isFieldsetGroup={{true}} + @updated={{@schemaBlockGroup.updated}} /> {{/each}} @@ -20,6 +21,7 @@ @renderStrategy={{@renderStrategy}} @isRequired={{this.isRequired}} @isFieldsetGroup={{false}} + @updated={{@schemaBlockGroup.updated}} /> {{/each}} {{/if}} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/component.ts b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/component.ts index 6f7d846cde8..54f53965413 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/component.ts +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/component.ts @@ -17,6 +17,7 @@ import pathJoin from 'ember-osf-web/utils/path-join'; import AbstractNodeModel from 'ember-osf-web/models/abstract-node'; import DraftRegistrationModel from 'ember-osf-web/models/draft-registration'; +import SchemaResponseModel from 'ember-osf-web/models/schema-response'; import styles from './styles'; import template from './template'; @@ -30,7 +31,8 @@ export default class Files extends Component { // Required param changeset!: BufferedChangeset; schemaBlock!: SchemaBlock; - draftRegistration!: DraftRegistrationModel; + draftRegistration?: DraftRegistrationModel; + revision?: SchemaResponseModel; @alias('schemaBlock.registrationResponseKey') valuePath!: string; @@ -42,7 +44,15 @@ export default class Files extends Component { @computed('draftRegistration', 'node.id') get nodeUrl() { - return pathJoin(baseURL, this.draftRegistration.belongsTo('branchedFrom').id()); + if (this.node) { + return pathJoin(baseURL, this.node.id); + } + return ''; + } + + @computed('revision', 'node', 'currentUserIsReadOnly') + get canEdit() { + return !this.revision && (this.node && !this.currentUserIsReadOnly); } didReceiveAttrs() { @@ -51,8 +61,8 @@ export default class Files extends Component { Boolean(this.changeset), ); assert( - 'Registries::SchemaBlockRenderer::Editable::Files requires a draft-registration to render', - Boolean(this.draftRegistration), + 'Registries::SchemaBlockRenderer::Editable::Files requires a draft-registration xor a revision to render', + Boolean(this.draftRegistration) !== Boolean(this.revision), ); assert( 'Registries::SchemaBlockRenderer::Editable::Files requires a valuePath to render', @@ -63,7 +73,11 @@ export default class Files extends Component { Boolean(this.schemaBlock), ); - this.node = this.draftRegistration.belongsTo('branchedFrom').value() as AbstractNodeModel; + if (this.draftRegistration) { + this.node = this.draftRegistration.belongsTo('branchedFrom').value() as AbstractNodeModel; + } else { + this.node = this.revision!.belongsTo('registration').value() as AbstractNodeModel; + } this.set('selectedFiles', this.changeset.get(this.valuePath) || []); } diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs index 3824a92f56e..b1768fcf123 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/files/template.hbs @@ -39,7 +39,7 @@ @onSelectFile={{action this.onSelectFile}} @onAddFile={{action this.onAddFile}} @onDeleteFile={{this.onDeleteFile}} - @canEdit={{and this.node (not this.currentUserIsReadOnly)}} + @canEdit={{this.canEdit}} disabled={{not this.node}} ...attributes /> diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/mapper/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/mapper/template.hbs index f8df6d26e4a..f8d16f38033 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/editable/mapper/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/editable/mapper/template.hbs @@ -53,6 +53,7 @@ 'registries/schema-block-renderer/editable/files' changeset=@changeset draftRegistration=@draftRegistration + revision=@revision onInput=@onInput ) )}} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss index 873f7ddf6ca..2a77792b34b 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/styles.scss @@ -9,6 +9,10 @@ margin-left: 0.25em; } +.Updated { + background-color: $brand-warning; + padding: 2px 10px; +} .HelpText { composes: Element from '../../styles'; diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs index 200d4e5f74a..7902eef16ef 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/label-content/template.hbs @@ -6,6 +6,9 @@ {{~#if (and @isRequired (not @readonly))~}} * {{~/if~}} + {{#if @updated}} + {{t 'osf-components.registries.schema-block-renderer/label.updated'}} + {{/if}}

{{#if @isEditableForm}}

diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/label/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/label/template.hbs index bc8c9091bbd..b7428e23891 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/label/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/label/template.hbs @@ -12,6 +12,7 @@ @readonly={{@readonly}} @isEditableForm={{@isEditableForm}} @draftManager={{@draftManager}} + @updated={{@updated}} /> {{else}} @@ -29,6 +30,7 @@ @isEditableForm={{@isEditableForm}} @draftManager={{@draftManager}} @showEditButton={{@showEditButton}} + @updated={{@updated}} /> {{/if}} diff --git a/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs b/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs index 7d52d2516cd..0c9172927df 100644 --- a/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs +++ b/lib/osf-components/addon/components/registries/schema-block-renderer/template.hbs @@ -9,6 +9,7 @@ @uniqueID={{@uniqueID}} @isRequired={{@isRequired}} @isFieldsetGroup={{@isFieldsetGroup}} + @updated={{@updated}} /> {{/let}} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/component.ts b/lib/osf-components/addon/components/registries/update-dropdown/component.ts new file mode 100644 index 00000000000..3b4d03a7c45 --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/component.ts @@ -0,0 +1,130 @@ +import { action } from '@ember/object'; +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 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 Media from 'ember-responsive'; +import Toast from 'ember-toastr/services/toast'; + +import { QueryHasManyResult } from 'ember-osf-web/models/osf-model'; +import RegistrationModel, { RegistrationReviewStates } from 'ember-osf-web/models/registration'; +import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; +import CurrentUserService from 'ember-osf-web/services/current-user'; +import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; + +interface Args { + registration: RegistrationModel; + selectedRevisionId: string; + isModeratorMode: boolean; +} + +export default class UpdateDropdown extends Component { + @service currentUser!: CurrentUserService; + @service intl!: Intl; + @service media!: Media; + @service toast!: Toast; + @service store!: Store; + @service router!: RouterService; + + @tracked showModal = false; + @tracked currentPage = 1; + @tracked totalPage = 1; + @tracked totalRevisions = 0; + @tracked revisions: QueryHasManyResult | SchemaResponseModel[] = []; + @tracked latestRevision!: SchemaResponseModel; + + isPendingCurrentUserApproval?: boolean; + + constructor(owner: unknown, args: Args) { + super(owner, args); + taskFor(this.getRevisionList).perform(); + } + + get isDesktop(): boolean { + return this.media.isDesktop || this.media.isJumbo; + } + + get hasMore() { + return this.currentPage <= this.totalPage; + } + + get shouldShowLoadMore() { + return !taskFor(this.getRevisionList).isRunning + && taskFor(this.getRevisionList).lastComplete + && this.hasMore; + } + + get shouldShowCreateButton(): boolean { + return Boolean(this.revisions.length) && !this.args.isModeratorMode + && this.args.registration.userHasAdminPermission + && [ + RegistrationReviewStates.Accepted, + RegistrationReviewStates.Embargo, + ].includes(this.args.registration.reviewsState!) + && this.args.registration.revisionState === RevisionReviewStates.Approved; + } + + get shouldShowUpdateLink(): boolean { + return Boolean(this.revisions.length) && !this.args.isModeratorMode + && this.args.registration.userHasReadPermission + && [ + RegistrationReviewStates.Accepted, + RegistrationReviewStates.Embargo, + ].includes(this.args.registration.reviewsState!) + && this.args.registration.revisionState !== RevisionReviewStates.Approved; + } + + get selectedRevisionIndex(): number { + return this.revisions.findIndex(revision => revision.id === this.args.selectedRevisionId); + } + + @action + showCreateModal() { + this.showModal = true; + } + + @action + closeCreateModal() { + this.showModal = false; + } + + @task + @waitFor + async getRevisionList() { + if (!this.args.registration){ + const notReistrationError = this.intl.t('registries.update_dropdown.not_a_registration_error'); + return this.toast.error(notReistrationError); + } + try { + if (this.hasMore) { + const currentPageResult = await this.args.registration.queryHasMany('schemaResponses', { + page: this.currentPage, + }); + this.totalPage = Math.ceil(currentPageResult.meta.total / currentPageResult.meta.per_page); + this.totalRevisions = currentPageResult.meta.total - 1; // -1 because the first revision is 0 + this.revisions.pushObjects(currentPageResult); + this.latestRevision = this.revisions[0]; + this.currentPage += 1; + } + } catch (e) { + const errorMessage = this.intl.t('registries.update_dropdown.revision_error_message'); + captureException(e, { errorMessage }); + this.toast.error(getApiErrorMessage(e), errorMessage); + } + } + + @task + @waitFor + async createNewSchemaResponse() { + const newRevision: SchemaResponseModel = this.store.createRecord('schema-response', { + registration: this.args.registration, + }); + await newRevision.save(); + this.router.transitionTo('registries.edit-revision', newRevision.id); + } +} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/list-item/component.ts b/lib/osf-components/addon/components/registries/update-dropdown/list-item/component.ts new file mode 100644 index 00000000000..dc7c38392ed --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/list-item/component.ts @@ -0,0 +1,24 @@ +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; +import Intl from 'ember-intl/services/intl'; +import SchemaResponseModel, { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; + +interface Args { + revision: SchemaResponseModel; + isModeratorMode: boolean; + index: number; + totalRevisions: number; +} + +export default class ListItem extends Component { + @service intl!: Intl; + + get shouldShow() { + const { revision, isModeratorMode } = this.args; + const visibleStates = [RevisionReviewStates.Approved]; + if (isModeratorMode) { + visibleStates.push(RevisionReviewStates.RevisionPendingModeration); + } + return revision.isOriginalResponse || visibleStates.includes(revision.reviewsState); + } +} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/list-item/styles.scss b/lib/osf-components/addon/components/registries/update-dropdown/list-item/styles.scss new file mode 100644 index 00000000000..26f516ea603 --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/list-item/styles.scss @@ -0,0 +1,27 @@ +.UpdateContainer { + padding-right: 10px; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 10px; + + &:hover { + background-color: rgba(#337ab7, 0.2); + } + + svg { + margin-right: 15px; + } +} + +.UpdateLink { + display: flex; + text-decoration: none; + + &:global(.active) { + font-weight: bold; + } + + &:hover { + text-decoration: none; + } +} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/list-item/template.hbs b/lib/osf-components/addon/components/registries/update-dropdown/list-item/template.hbs new file mode 100644 index 00000000000..49e24b7378c --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/list-item/template.hbs @@ -0,0 +1,11 @@ +{{#if this.shouldShow}} +

+ {{!-- Using LinkTo instead of OsfLink due to how queryParams are buggy for setting active state --}} + + + +
+{{/if}} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/styles.scss b/lib/osf-components/addon/components/registries/update-dropdown/styles.scss new file mode 100644 index 00000000000..5989a5f91bd --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/styles.scss @@ -0,0 +1,38 @@ +.ResponsiveDropdown { + margin-left: 63px; +} + +.Updates { + padding: 15px; + border: 1px solid #ddd; + border-radius: 2px; + background-color: #fff; + background-size: cover; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + max-width: 450px; + overflow-y: auto; + height: auto; + max-height: 400px; +} + +.Update { + padding: 5px; +} + +.UpdateText { + color: #fff; + font-weight: 600; + padding-left: 10px; + text-decoration: none; + + &:hover, + &:active, + &:focus { + outline: 0; + } +} + +.UpdateContainerText { + display: flex; + flex-direction: column; +} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/template.hbs b/lib/osf-components/addon/components/registries/update-dropdown/template.hbs new file mode 100644 index 00000000000..a2352bdfc95 --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/template.hbs @@ -0,0 +1,71 @@ + + + {{#if (gt this.selectedRevisionIndex -1)}} + + {{else}} + {{t 'registries.update_dropdown.dropdown_title'}} + {{/if}} + {{fa-icon 'caret-down'}} + + +
+ {{#if this.revisions}} + {{#each this.revisions as |revision index|}} + + {{/each}} + {{#if this.shouldShowLoadMore}} +
+ {{t 'registries.update_dropdown.load_more'}} +
+ {{/if}} + {{else}} + {{t 'registries.update_dropdown.no_revisions_error'}} + {{/if}} + {{#if this.shouldShowCreateButton}} + + {{else if this.shouldShowUpdateLink}} + + + + {{/if}} +
+
+
+ + diff --git a/lib/osf-components/addon/components/registries/update-dropdown/update-label/component.ts b/lib/osf-components/addon/components/registries/update-dropdown/update-label/component.ts new file mode 100644 index 00000000000..981434431de --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/update-label/component.ts @@ -0,0 +1,24 @@ +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; +import Intl from 'ember-intl/services/intl'; + +interface Args { + totalRevisions: number; + index: number; +} + +export default class UpdateLabel extends Component { + @service intl!: Intl; + + get label() { + const { totalRevisions, index } = this.args; + const revisionNumber = totalRevisions - index; + if (index === totalRevisions) { + return this.intl.t('registries.update_dropdown.updates_list_label_original'); + } + if (index === 0) { + return this.intl.t('registries.update_dropdown.latest'); + } + return this.intl.t('registries.update_dropdown.updates_list_label', { revisionNumber }); + } +} diff --git a/lib/osf-components/addon/components/registries/update-dropdown/update-label/template.hbs b/lib/osf-components/addon/components/registries/update-dropdown/update-label/template.hbs new file mode 100644 index 00000000000..6374d320941 --- /dev/null +++ b/lib/osf-components/addon/components/registries/update-dropdown/update-label/template.hbs @@ -0,0 +1 @@ +{{this.label}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/registries/version-metadata/styles.scss b/lib/osf-components/addon/components/registries/version-metadata/styles.scss new file mode 100644 index 00000000000..23147059912 --- /dev/null +++ b/lib/osf-components/addon/components/registries/version-metadata/styles.scss @@ -0,0 +1,8 @@ +.versionMetadata { + border: 3px solid $brand-warning; + padding: 10px; +} + +.versionMetadata-title { + font-weight: bold; +} diff --git a/lib/osf-components/addon/components/registries/version-metadata/template.hbs b/lib/osf-components/addon/components/registries/version-metadata/template.hbs new file mode 100644 index 00000000000..674a0ffad5c --- /dev/null +++ b/lib/osf-components/addon/components/registries/version-metadata/template.hbs @@ -0,0 +1,33 @@ +{{assert 'Registries::VersionMetadata requires @revision' @revision}} +
+

+ {{if @revision.isOriginalResponse + (t 'registries.overview.versionMetadata.originalTitle') + (t 'registries.overview.versionMetadata.updateTitle') + }} +

+ {{!-- TODO: add description for what fields have changed --}} +

+ {{if @revision.isOriginalResponse + (t 'registries.overview.versionMetadata.originalDate' date=(moment-format @revision.dateModified 'MMM DD, YYYY')) + (t 'registries.overview.versionMetadata.date' date=(moment-format @revision.dateModified 'MMM DD, YYYY')) + }} +

+ {{#if (and @revision (not @revision.isOriginalResponse))}} +

+ {{t 'registries.overview.versionMetadata.reason'}} +
+ + {{if @revision.revisionJustification + @revision.revisionJustification + (t 'registries.overview.versionMetadata.noReason') + }} + +

+ {{/if}} +
diff --git a/lib/osf-components/app/components/registries/new-update-modal/component.js b/lib/osf-components/app/components/registries/new-update-modal/component.js new file mode 100644 index 00000000000..6ebc5752dea --- /dev/null +++ b/lib/osf-components/app/components/registries/new-update-modal/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/new-update-modal/component'; diff --git a/lib/osf-components/app/components/registries/new-update-modal/template.js b/lib/osf-components/app/components/registries/new-update-modal/template.js new file mode 100644 index 00000000000..128e5f67170 --- /dev/null +++ b/lib/osf-components/app/components/registries/new-update-modal/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/new-update-modal/template'; diff --git a/lib/osf-components/app/components/registries/revised-responses-list/component.js b/lib/osf-components/app/components/registries/revised-responses-list/component.js new file mode 100644 index 00000000000..317dcebd98a --- /dev/null +++ b/lib/osf-components/app/components/registries/revised-responses-list/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/revised-responses-list/component'; diff --git a/lib/osf-components/app/components/registries/revised-responses-list/template.js b/lib/osf-components/app/components/registries/revised-responses-list/template.js new file mode 100644 index 00000000000..3930a8eb60b --- /dev/null +++ b/lib/osf-components/app/components/registries/revised-responses-list/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/revised-responses-list/template'; diff --git a/lib/osf-components/app/components/registries/revision-justification-renderer/label-display/template.js b/lib/osf-components/app/components/registries/revision-justification-renderer/label-display/template.js new file mode 100644 index 00000000000..34128155dd5 --- /dev/null +++ b/lib/osf-components/app/components/registries/revision-justification-renderer/label-display/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/revision-justification-renderer/label-display/template'; diff --git a/lib/osf-components/app/components/registries/revision-justification-renderer/template.js b/lib/osf-components/app/components/registries/revision-justification-renderer/template.js new file mode 100644 index 00000000000..48b96afac66 --- /dev/null +++ b/lib/osf-components/app/components/registries/revision-justification-renderer/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/revision-justification-renderer/template'; diff --git a/lib/osf-components/app/components/registries/update-dropdown/component.js b/lib/osf-components/app/components/registries/update-dropdown/component.js new file mode 100644 index 00000000000..dfc63e9b341 --- /dev/null +++ b/lib/osf-components/app/components/registries/update-dropdown/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/update-dropdown/component'; diff --git a/lib/osf-components/app/components/registries/update-dropdown/list-item/component.js b/lib/osf-components/app/components/registries/update-dropdown/list-item/component.js new file mode 100644 index 00000000000..0dbcb0fdd0a --- /dev/null +++ b/lib/osf-components/app/components/registries/update-dropdown/list-item/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/update-dropdown/list-item/component'; diff --git a/lib/osf-components/app/components/registries/update-dropdown/list-item/template.js b/lib/osf-components/app/components/registries/update-dropdown/list-item/template.js new file mode 100644 index 00000000000..1cfe810150e --- /dev/null +++ b/lib/osf-components/app/components/registries/update-dropdown/list-item/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/update-dropdown/list-item/template'; diff --git a/lib/osf-components/app/components/registries/update-dropdown/template.js b/lib/osf-components/app/components/registries/update-dropdown/template.js new file mode 100644 index 00000000000..f49b1e4b0c2 --- /dev/null +++ b/lib/osf-components/app/components/registries/update-dropdown/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/update-dropdown/template'; diff --git a/lib/osf-components/app/components/registries/update-dropdown/update-label/component.js b/lib/osf-components/app/components/registries/update-dropdown/update-label/component.js new file mode 100644 index 00000000000..dd77105f00c --- /dev/null +++ b/lib/osf-components/app/components/registries/update-dropdown/update-label/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/update-dropdown/update-label/component'; diff --git a/lib/osf-components/app/components/registries/update-dropdown/update-label/template.js b/lib/osf-components/app/components/registries/update-dropdown/update-label/template.js new file mode 100644 index 00000000000..56df5cbab3a --- /dev/null +++ b/lib/osf-components/app/components/registries/update-dropdown/update-label/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/update-dropdown/update-label/template'; diff --git a/lib/osf-components/app/components/registries/version-metadata/template.js b/lib/osf-components/app/components/registries/version-metadata/template.js new file mode 100644 index 00000000000..2afac22fc15 --- /dev/null +++ b/lib/osf-components/app/components/registries/version-metadata/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/registries/version-metadata/template'; diff --git a/lib/registries/addon/branded/moderation/index/route.ts b/lib/registries/addon/branded/moderation/index/route.ts index 086652e440f..207310d8938 100644 --- a/lib/registries/addon/branded/moderation/index/route.ts +++ b/lib/registries/addon/branded/moderation/index/route.ts @@ -2,6 +2,6 @@ import Route from '@ember/routing/route'; export default class BrandedModerationIndexRoute extends Route { beforeModel() { - this.replaceWith('branded.moderation.submissions'); + this.replaceWith('branded.moderation.pending'); } } diff --git a/lib/registries/addon/branded/moderation/pending/controller.ts b/lib/registries/addon/branded/moderation/pending/controller.ts new file mode 100644 index 00000000000..8d0fc566e54 --- /dev/null +++ b/lib/registries/addon/branded/moderation/pending/controller.ts @@ -0,0 +1,21 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; +import { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; + +type PendingStates = + RegistrationReviewStates.Pending | + RegistrationReviewStates.PendingWithdrawRequest | + RevisionReviewStates.RevisionPendingModeration; +export default class RegistriesModerationPendingController extends Controller { + queryParams = ['state']; + + @tracked state?: PendingStates; + + @action + changeTab(tab: PendingStates) { + this.state = tab; + } +} diff --git a/lib/registries/addon/branded/moderation/pending/route.ts b/lib/registries/addon/branded/moderation/pending/route.ts new file mode 100644 index 00000000000..db72890e7e0 --- /dev/null +++ b/lib/registries/addon/branded/moderation/pending/route.ts @@ -0,0 +1,38 @@ +import { action } from '@ember/object'; +import Transition from '@ember/routing/-private/transition'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; +import RegistrationProviderModel from 'ember-osf-web/models/registration-provider'; +import { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; +import Analytics from 'ember-osf-web/services/analytics'; + +import RegistriesModerationPendingController from './controller'; + +export default class BrandedModerationPendingRoute extends Route { + @service analytics!: Analytics; + + setupController( + controller: RegistriesModerationPendingController, + model: RegistrationProviderModel, + transition: Transition, + ) { + super.setupController(controller, model, transition); + const { state } = controller; + if (!state + || ![ + RegistrationReviewStates.Pending, + RegistrationReviewStates.PendingWithdraw, + RevisionReviewStates.RevisionPendingModeration, + ].includes(state!)) { + controller.set('state', 'pending'); + this.replaceWith('branded.moderation.pending'); + } + } + + @action + didTransition() { + this.analytics.trackPage(); + } +} diff --git a/lib/registries/addon/branded/moderation/submissions/styles.scss b/lib/registries/addon/branded/moderation/pending/styles.scss similarity index 100% rename from lib/registries/addon/branded/moderation/submissions/styles.scss rename to lib/registries/addon/branded/moderation/pending/styles.scss diff --git a/lib/registries/addon/branded/moderation/pending/template.hbs b/lib/registries/addon/branded/moderation/pending/template.hbs new file mode 100644 index 00000000000..80cdd9c219f --- /dev/null +++ b/lib/registries/addon/branded/moderation/pending/template.hbs @@ -0,0 +1,52 @@ +{{page-title (t 'registries.moderation.pending.title') prepend=false}} +{{!-- TODO: Add number of registrations in each state in tab --}} + +
+
+ + {{#if this.model.allowUpdates}} + + {{/if}} + +
+ +
+ +
diff --git a/lib/registries/addon/branded/moderation/submissions/controller.ts b/lib/registries/addon/branded/moderation/submissions/controller.ts deleted file mode 100644 index a7144dc391f..00000000000 --- a/lib/registries/addon/branded/moderation/submissions/controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Controller from '@ember/controller'; -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; - -import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; - -export default class RegistriesModerationSubmissionController extends Controller { - queryParams = ['state']; - - @tracked state?: RegistrationReviewStates; - - @action - changeTab(tab: RegistrationReviewStates) { - this.state = tab; - } -} diff --git a/lib/registries/addon/branded/moderation/submitted/controller.ts b/lib/registries/addon/branded/moderation/submitted/controller.ts new file mode 100644 index 00000000000..e5d858ca493 --- /dev/null +++ b/lib/registries/addon/branded/moderation/submitted/controller.ts @@ -0,0 +1,22 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; + +type SubmittedStates = + RegistrationReviewStates.Accepted | + RegistrationReviewStates.Embargo | + RegistrationReviewStates.Withdrawn | + RegistrationReviewStates.Rejected; + +export default class RegistriesModerationSubmittedController extends Controller { + queryParams = ['state']; + + @tracked state?: SubmittedStates; + + @action + changeTab(tab: SubmittedStates) { + this.state = tab; + } +} diff --git a/lib/registries/addon/branded/moderation/submissions/route.ts b/lib/registries/addon/branded/moderation/submitted/route.ts similarity index 70% rename from lib/registries/addon/branded/moderation/submissions/route.ts rename to lib/registries/addon/branded/moderation/submitted/route.ts index ea92a82337b..9a7a87293a6 100644 --- a/lib/registries/addon/branded/moderation/submissions/route.ts +++ b/lib/registries/addon/branded/moderation/submitted/route.ts @@ -7,13 +7,13 @@ import { RegistrationReviewStates } from 'ember-osf-web/models/registration'; import RegistrationProviderModel from 'ember-osf-web/models/registration-provider'; import Analytics from 'ember-osf-web/services/analytics'; -import RegistriesModerationSubmissionController from './controller'; +import RegistriesModerationSubmittedController from './controller'; -export default class BrandedModerationSubmissionsRoute extends Route { +export default class BrandedModerationSubmittedRoute extends Route { @service analytics!: Analytics; setupController( - controller: RegistriesModerationSubmissionController, + controller: RegistriesModerationSubmittedController, model: RegistrationProviderModel, transition: Transition, ) { @@ -21,15 +21,13 @@ export default class BrandedModerationSubmissionsRoute extends Route { const { state } = controller; if (!state || ![ - RegistrationReviewStates.Pending, RegistrationReviewStates.Accepted, RegistrationReviewStates.Embargo, RegistrationReviewStates.Rejected, RegistrationReviewStates.Withdrawn, - RegistrationReviewStates.PendingWithdraw, ].includes(state!)) { - controller.set('state', 'pending'); - this.replaceWith('branded.moderation.submissions'); + controller.set('state', RegistrationReviewStates.Accepted); + this.replaceWith('branded.moderation.submitted'); } } diff --git a/lib/registries/addon/branded/moderation/submitted/styles.scss b/lib/registries/addon/branded/moderation/submitted/styles.scss new file mode 100644 index 00000000000..2d4adc5ee86 --- /dev/null +++ b/lib/registries/addon/branded/moderation/submitted/styles.scss @@ -0,0 +1,50 @@ +.contentBorder { + composes: childNav from '../styles.scss'; +} + +.registrationListContainer { + ul { + margin: 0; + border: 1px solid $color-border-gray; + } + + li { + background-color: initial; + border-left: 0; + border-right: 0; + border-top: 0; + } + + li:last-of-type { + border-bottom: 0; + } +} + +.tabSortingWrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: 15px; + background-color: $color-bg-gray-light; + border-bottom: 1px solid $color-border-gray; + padding: 10px 20px; +} + +.tabs { + position: relative; +} + +.tab { + height: 30px; + border: 0; + background: none; + + &.selected { + font-weight: 700; + position: relative; + } +} + +.sortButton { + margin-right: 15px; +} diff --git a/lib/registries/addon/branded/moderation/submissions/template.hbs b/lib/registries/addon/branded/moderation/submitted/template.hbs similarity index 70% rename from lib/registries/addon/branded/moderation/submissions/template.hbs rename to lib/registries/addon/branded/moderation/submitted/template.hbs index 3f1e1384f5a..04733ad0846 100644 --- a/lib/registries/addon/branded/moderation/submissions/template.hbs +++ b/lib/registries/addon/branded/moderation/submitted/template.hbs @@ -1,4 +1,4 @@ -{{page-title (t 'registries.moderation.submissions.title') prepend=false}} +{{page-title (t 'registries.moderation.submitted.title') prepend=false}} {{!-- TODO: Add number of registrations in each state in tab --}}
- -
{{!-- TODO: add pending count in Submissions and Withdrawals tab --}} - {{#let 'registries.branded.moderation.submissions' as |submissionsRoute|}} - + {{#let 'registries.branded.moderation.submitted' as |submittedRoute|}} + - {{t 'registries.moderation.submissions.title'}} + {{t 'registries.moderation.submitted.title'}} + + + {{/let}} + {{#let 'registries.branded.moderation.pending' as |pendingRoute|}} + + + {{t 'registries.moderation.pending.title'}} {{/let}} diff --git a/lib/registries/addon/components/make-decision-dropdown/component.ts b/lib/registries/addon/components/make-decision-dropdown/component.ts index 15ad7df99e8..73f7e38e1a6 100644 --- a/lib/registries/addon/components/make-decision-dropdown/component.ts +++ b/lib/registries/addon/components/make-decision-dropdown/component.ts @@ -11,12 +11,20 @@ import Toast from 'ember-toastr/services/toast'; import RouterService from '@ember/routing/router-service'; import RegistrationModel, -{ RegistrationReviewStates, reviewsStateToDecisionMap } from 'ember-osf-web/models/registration'; +{ + RegistrationReviewStates, + reviewsStateToDecisionMap, + NonActionableRegistrationStates, + ActionableRevisionStates, +} from 'ember-osf-web/models/registration'; import { ReviewActionTrigger } from 'ember-osf-web/models/review-action'; import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; +import { RevisionReviewStates } from 'ember-osf-web/models/schema-response'; +import { SchemaResponseActionTrigger } from 'ember-osf-web/models/schema-response-action'; interface Args { registration: RegistrationModel; + selectedRevisionId: string; } export default class MakeDecisionDropdown extends Component { @@ -25,7 +33,7 @@ export default class MakeDecisionDropdown extends Component { @service toast!: Toast; @service router!: RouterService; - @tracked decisionTrigger?: ReviewActionTrigger; + @tracked decisionTrigger?: ReviewActionTrigger | SchemaResponseActionTrigger; @tracked comment?: string; reviewsStateToDecisionMap = reviewsStateToDecisionMap; @@ -39,6 +47,10 @@ export default class MakeDecisionDropdown extends Component { this.intl.t('registries.makeDecisionDropdown.acceptWithdrawalDescription'), [ReviewActionTrigger.RejectWithdrawal]: this.intl.t('registries.makeDecisionDropdown.rejectWithdrawalDescription'), + [SchemaResponseActionTrigger.AcceptRevision]: + this.intl.t('registries.makeDecisionDropdown.acceptRevisionDescription'), + [SchemaResponseActionTrigger.RejectRevision]: + this.intl.t('registries.makeDecisionDropdown.rejectRevisionDescription'), }; actionTriggerToTextMap = { @@ -47,12 +59,27 @@ export default class MakeDecisionDropdown extends Component { [ReviewActionTrigger.RejectSubmission]: this.intl.t('registries.makeDecisionDropdown.rejectSubmission'), [ReviewActionTrigger.AcceptWithdrawal]: this.intl.t('registries.makeDecisionDropdown.acceptWithdrawal'), [ReviewActionTrigger.RejectWithdrawal]: this.intl.t('registries.makeDecisionDropdown.rejectWithdrawal'), + [SchemaResponseActionTrigger.AcceptRevision]: + this.intl.t('registries.makeDecisionDropdown.acceptRevision'), + [SchemaResponseActionTrigger.RejectRevision]: + this.intl.t('registries.makeDecisionDropdown.rejectRevision'), }; + get latestRevision() { + return this.args.registration.schemaResponses.firstObject; + } + + get revisionIsPending() { + return (this.args.registration.reviewsState === RegistrationReviewStates.Accepted + || this.args.registration.reviewsState === RegistrationReviewStates.Embargo) + && this.args.registration.revisionState === RevisionReviewStates.RevisionPendingModeration; + } + get commentTextArea() { if (this.args.registration.reviewsState) { if ([RegistrationReviewStates.Pending, RegistrationReviewStates.PendingWithdraw] - .includes(this.args.registration.reviewsState)) { + .includes(this.args.registration.reviewsState) || + this.revisionIsPending) { return { label: this.intl.t('registries.makeDecisionDropdown.additionalComment'), placeholder: this.intl.t('registries.makeDecisionDropdown.additionalCommentPlaceholder'), @@ -72,32 +99,56 @@ export default class MakeDecisionDropdown extends Component { } get hasModeratorActions() { - return this.args.registration.reviewsState + return (this.args.registration.reviewsState && ![ RegistrationReviewStates.Initial, RegistrationReviewStates.Withdrawn, RegistrationReviewStates.Rejected, - ].includes(this.args.registration.reviewsState); + ].includes(this.args.registration.reviewsState)) + || this.revisionIsPending; + } + + get moderatorActions() { + const reviewsState = + this.args.registration.reviewsState as Exclude; + const revisionState = this.args.registration.revisionState as ActionableRevisionStates; + let actions = reviewsState ? reviewsStateToDecisionMap[reviewsState] : []; + if (this.revisionIsPending) { + actions = reviewsStateToDecisionMap[revisionState]; + } + return actions; } @task @waitFor async submitDecision() { if (this.decisionTrigger) { - const newAction = this.store.createRecord('review-action', { + const isSchemaResponseAction = ([ + SchemaResponseActionTrigger.RejectRevision, SchemaResponseActionTrigger.AcceptRevision, + ] as Array).includes(this.decisionTrigger); + const actionType = isSchemaResponseAction ? 'schema-response-action' : 'review-action'; + const target = isSchemaResponseAction ? this.args.registration.schemaResponses.firstObject + : this.args.registration; + const newAction = this.store.createRecord(actionType, { actionTrigger: this.decisionTrigger, comment: (this.comment ? this.comment : undefined), - target: this.args.registration, + target, }); try { await newAction.save(); this.toast.success(this.intl.t('registries.makeDecisionDropdown.success')); if (this.decisionTrigger === ReviewActionTrigger.RejectSubmission) { this.router.transitionTo( - 'registries.branded.moderation.submissions', + 'registries.branded.moderation.submitted', this.args.registration.provider.get('id'), { queryParams: { state: RegistrationReviewStates.Rejected } }, ); + } else if (this.decisionTrigger === SchemaResponseActionTrigger.RejectRevision) { + this.router.transitionTo( + 'registries.branded.moderation.submitted', + this.args.registration.provider.get('id'), + { queryParams: { state: RevisionReviewStates.RevisionPendingModeration } }, + ); } this.args.registration.reload(); } catch (e) { diff --git a/lib/registries/addon/components/make-decision-dropdown/styles.scss b/lib/registries/addon/components/make-decision-dropdown/styles.scss index b94c2090c1f..64c38b7ad15 100644 --- a/lib/registries/addon/components/make-decision-dropdown/styles.scss +++ b/lib/registries/addon/components/make-decision-dropdown/styles.scss @@ -34,5 +34,6 @@ } .SubmitButton { + margin: 10px 0; float: right; } diff --git a/lib/registries/addon/components/make-decision-dropdown/template.hbs b/lib/registries/addon/components/make-decision-dropdown/template.hbs index 1606d13d6fd..eb191470fb0 100644 --- a/lib/registries/addon/components/make-decision-dropdown/template.hbs +++ b/lib/registries/addon/components/make-decision-dropdown/template.hbs @@ -14,14 +14,19 @@ - + {{#if this.revisionIsPending}} + + {{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.onClickRegister.isRunning}} - - {{else}} - {{t 'registries.drafts.draft.register'}} - {{/if}} - +{{#if this.currentUserIsAdmin}} + + {{#if this.onClickRegister.isRunning}} + + {{else}} + {{t 'registries.drafts.draft.register'}} + {{/if}} + +{{/if}} +{{#if (and (not this.currentUserIsAdmin) (not @showMobileView))}} +
+ {{t 'registries.drafts.draft.review.non_admin_warning'}} +
+{{/if}} {{#if (and this.isInvalid (not @showMobileView))}}
{{t 'registries.drafts.draft.review.invalid_warning'}} diff --git a/lib/registries/addon/drafts/draft/review/template.hbs b/lib/registries/addon/drafts/draft/review/template.hbs index e26a4ef7bc1..5a11b7ba6f8 100644 --- a/lib/registries/addon/drafts/draft/review/template.hbs +++ b/lib/registries/addon/drafts/draft/review/template.hbs @@ -7,6 +7,14 @@
+ {{#if (and (not draftManager.currentUserIsAdmin) this.showMobileView)}} +
+ {{t 'registries.drafts.draft.review.non_admin_warning'}} +
+ {{/if}} {{#if (and (not draftManager.registrationResponsesIsValid) this.showMobileView)}}
diff --git a/lib/registries/addon/drafts/draft/styles.scss b/lib/registries/addon/drafts/draft/styles.scss index 77ba8ff8d68..8ff98a2576e 100644 --- a/lib/registries/addon/drafts/draft/styles.scss +++ b/lib/registries/addon/drafts/draft/styles.scss @@ -117,19 +117,6 @@ width: 100%; } -.MobileNavPageLabelSection { - min-width: 0; - width: 100%; -} - -.MobileNavPageLabel { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 600; - color: $color-text-black; -} - @media screen and (min-width: 992px) { .ContentBackground { display: flex; diff --git a/lib/registries/addon/edit-revision/-components/continue-edit-modal/component.ts b/lib/registries/addon/edit-revision/-components/continue-edit-modal/component.ts new file mode 100644 index 00000000000..5222198e903 --- /dev/null +++ b/lib/registries/addon/edit-revision/-components/continue-edit-modal/component.ts @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class ContinueEditModal extends Component { + reason = ''; +} diff --git a/lib/registries/addon/edit-revision/-components/continue-edit-modal/styles.scss b/lib/registries/addon/edit-revision/-components/continue-edit-modal/styles.scss new file mode 100644 index 00000000000..5ca23a8a94c --- /dev/null +++ b/lib/registries/addon/edit-revision/-components/continue-edit-modal/styles.scss @@ -0,0 +1,10 @@ +.reasonWrapper { + padding: 20px 0 0; +} + +.textArea { + display: block; + min-height: 100px; + min-width: 300px; + width: 100%; +} diff --git a/lib/registries/addon/edit-revision/-components/continue-edit-modal/template.hbs b/lib/registries/addon/edit-revision/-components/continue-edit-modal/template.hbs new file mode 100644 index 00000000000..e886f6a390a --- /dev/null +++ b/lib/registries/addon/edit-revision/-components/continue-edit-modal/template.hbs @@ -0,0 +1,42 @@ + + + {{t 'registries.edit_revision.continue_edit_modal.heading'}} + + + {{t 'registries.edit_revision.continue_edit_modal.warning'}} + {{#let (unique-id) as |reasonInputId|}} +
+ +