diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index dcd852039..000000000 --- a/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ['@vue/cli-plugin-babel/preset'], -}; diff --git a/index.html b/index.html deleted file mode 100644 index 88d0bda8e..000000000 --- a/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - UI Library - Public Knowledge Project - - - - - - - -
- - - - - diff --git a/public/globals.js b/public/globals.js index 388dafe3d..0b1c9cd61 100644 --- a/public/globals.js +++ b/public/globals.js @@ -37,6 +37,7 @@ window.pkp = { ASSOC_TYPE_SECTION: 530, ASSOC_TYPE_SERIES: 530, // OMP - always matches ASSOC_TYPE_SECTION REVIEW_ASSIGNMENT_STATUS_AWAITING_RESPONSE: 0, + REVIEW_ASSIGNMENT_STATUS_DECLINED: 1, REVIEW_ASSIGNMENT_STATUS_RESPONSE_OVERDUE: 4, REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE: 6, REVIEW_ASSIGNMENT_STATUS_ACCEPTED: 5, @@ -91,16 +92,16 @@ window.pkp = { * Icon map for document types */ documentTypeIcons: { - default: 'file-o', // DOCUMENT_TYPE_DEFAULT - audio: 'file-audio-o', // DOCUMENT_TYPE_AUDIO - epub: 'file-text-o', // DOCUMENT_TYPE_EPUB - excel: 'file-excel-o', // DOCUMENT_TYPE_EXCEL - html: 'file-code-o', // DOCUMENT_TYPE_HTML - image: 'file-image-o', // DOCUMENT_TYPE_IMAGE - pdf: 'file-pdf-o', // DOCUMENT_TYPE_PDF - word: 'file-word-o', // DOCUMENT_TYPE_WORD - video: 'file-video-o', // DOCUMENT_TYPE_VIDEO - zip: 'file-archive-o', // DOCUMENT_TYPE_ZIP + default: 'DefaultDocument', // DOCUMENT_TYPE_DEFAULT + audio: 'FileAudio', // DOCUMENT_TYPE_AUDIO + epub: 'FileEpub', // DOCUMENT_TYPE_EPUB + excel: 'FileExcel', // DOCUMENT_TYPE_EXCEL + html: 'FileHtml', // DOCUMENT_TYPE_HTML + image: 'FileImage', // DOCUMENT_TYPE_IMAGE + pdf: 'FilePdf', // DOCUMENT_TYPE_PDF + word: 'FileDoc', // DOCUMENT_TYPE_WORD + video: 'FileVideo', // DOCUMENT_TYPE_VIDEO + zip: 'file-FileZip-o', // DOCUMENT_TYPE_ZIP }, /** @@ -108,29 +109,39 @@ window.pkp = { */ localeKeys: { 'article.article': 'Article', + 'common.abstract': 'Abstract', + 'common.addCCBCC': 'Add CC/BCC', 'common.attachFiles': 'Attach Files', + 'common.attachedFiles': 'Attached Files', 'common.cancel': 'Cancel', 'common.clearSearch': 'Clear search phrase', 'common.close': 'Close', 'common.commaListSeparator': ', ', + 'common.content': 'Content', 'common.delete': 'Delete', 'common.description': 'Description', + 'common.deselect': 'Deselect', 'common.download': 'Download', 'common.edit': 'Edit', 'common.editItem': 'Edit {$name}', + 'common.emailTemplates': 'Email Templates', 'common.error': 'Error', 'common.filter': 'Filters', 'common.filterAdd': 'Add filter: {$filterTitle}', 'common.filterRemove': 'Clear filter: {$filterTitle}', 'common.filtersClear': 'Clear Filters', - 'common.inParenthesis': '({$text})', + 'common.findTemplate': 'Find Template', + 'common.insert': 'Insert', 'common.insertContent': 'Insert Content', + 'common.insertContentSearch': 'Find content to insert', + 'common.keywords': 'Keywords', 'common.lastActivity': 'Last activity recorded on {$date}.', 'common.loaded': 'Loaded', 'common.loading': 'Loading', 'common.no': 'No', 'common.noItemsFound': 'No items found.', 'common.none': 'None', + 'common.numberedMore': '{$number} more', 'common.ok': 'OK', 'common.order': 'Order', 'common.orderDown': 'Decrease position of {$itemTitle}', @@ -141,17 +152,22 @@ window.pkp = { 'common.pagination.next': 'Next', 'common.pagination.previous': 'Previous', 'common.remove': 'Remove', + 'common.removeItem': 'Remove {$item}', 'common.required': 'Required', - 'common.reviewRoundNumber': 'Round {$round}', 'common.save': 'Save', 'common.saving': 'Saving', 'common.search': 'Search', + 'common.searching': 'Searching', 'common.selectAll': 'Select All', 'common.selectNone': 'Select None', 'common.selectWithName': 'Select {$name}', + 'common.showingSteps': '{$current}/{$total} steps', 'common.showingXofX': 'Showing {$start} to {$finish} of {$total}', 'common.status': 'Status', + 'common.subtitle': 'Subtitle', + 'common.switchTo': 'Switch to', + 'common.switchToNamedItem': 'Switch to {$name}', 'common.type': 'Type', 'common.unknownError': 'An unexpected error has occurred. Please reload the page and try again.', @@ -161,14 +177,157 @@ window.pkp = { 'common.view': 'View', 'common.viewWithName': 'View {$name}', 'common.yes': 'Yes', + 'dashboard.acceptOrDeclineRequestDate': + 'Please accept or decline this request {$date}', + 'dashboard.action': 'Action', + 'dashboard.applyFilters': 'Apply Filters', + 'dashboard.assignEditor': 'Assign Editor', + 'dashboard.assignReviewers': 'Assign Reviewers', + 'dashboard.clearFilters': 'Clear Filters', + 'dashboard.completeReviewByDate': 'Please complete this review by {$date}.', + 'dashboard.dashboards': 'Dashboards', + 'dashboard.deadlineForComplitingReviewHasPassed': + 'Deadline for completing this review has passed. Please complete the review at the earliest.', + 'dashboard.deadlineForRespondingAcceptOrDecline': + 'Deadline for responding to this request has passed. Please accept or decline this request at the earliest.', + 'dashboard.mySubmissions': 'My Submissions', + 'dashboard.newReviewRoundToBeCreated': 'New review round to be created', + 'dashboard.reviewAssignment.action.cancelReviewer': 'Cancel Reviewer', + 'dashboard.reviewAssignment.action.editDueDate': 'Edit Due Date', + 'dashboard.reviewAssignment.action.resendReviewRequest': + 'Resend Review Request', + 'dashboard.reviewAssignment.action.unassignReviewer': 'Unassign', + 'dashboard.reviewAssignment.action.viewDetails': 'View details', + 'dashboard.reviewAssignment.action.viewRecommendation': + 'View recommendation', + 'dashboard.reviewAssignment.action.viewUnreadRecommendation': + 'View unread recommendation', + 'dashboard.reviewAssignment.statusAccepted.description': + 'This reviewer has accepted the review request. Their review is due in {$days} days on {$date}.', + 'dashboard.reviewAssignment.statusAccepted.title': + 'Ongoing review - request accepted', + 'dashboard.reviewAssignment.statusAwaitingResponse.description': + 'Review request has been shared with reviewer. Awaiting response in {$days} days on {$date}', + 'dashboard.reviewAssignment.statusAwaitingResponse.title': + 'Awaiting Response from the reviewer', + 'dashboard.reviewAssignment.statusCancelled.description': + 'Reviewer has cancelled the review request on {$date}', + 'dashboard.reviewAssignment.statusCancelled.title': + 'Reviewer cancelled review request', + 'dashboard.reviewAssignment.statusComplete.description': + 'The review was accepted by the editor on {$date}.', + 'dashboard.reviewAssignment.statusComplete.title': + 'Review was confirmed by editor', + 'dashboard.reviewAssignment.statusDeclined.description': + 'Reviewer declined the review request on {$date}', + 'dashboard.reviewAssignment.statusDeclined.title': + 'Review Request declined on {$date}', + 'dashboard.reviewAssignment.statusReceived.description': + 'The review was completed on {$date} with the following recommendation: {$recommendation}', + 'dashboard.reviewAssignment.statusReceived.title': + 'Review completed on {$date}', + 'dashboard.reviewAssignment.statusRequestResend.description': + 'Review request has been reshared with reviewer. Awaiting response in {$days} days on {$date}', + 'dashboard.reviewAssignment.statusRequestResend.title': + 'Awaiting Response from the reviewer', + 'dashboard.reviewAssignment.statusResponseOverdue.description': + 'This reviewer has not responded to the review request. A response was due on {$date}', + 'dashboard.reviewAssignment.statusResponseOverdue.title': + 'Review Request overdue by {$days} days', + 'dashboard.reviewAssignment.statusReviewOverdue.description': + 'This reviewer has not completed their review. A response was due on {$date}.', + 'dashboard.reviewAssignment.statusReviewOverdue.title': + 'Review overdue by {$days} days', + 'dashboard.reviewAssignments': 'Review Assignments', + 'dashboard.reviewUpdateCounts': + 'Review update {$reviewsCompletedCount}/{$reviewsTotalCount}', + 'dashboard.reviewersAssigned': 'Reviewers assigned', + 'dashboard.revisionRequested': 'Revision requested', + 'dashboard.revisionRequestedFromAuthor': 'Revisions requested from author', + 'dashboard.revisionsRequestedFromAuthorNextRound': + 'Revisions requested from the author to be taken to a new review round', + 'dashboard.revisionsSubmittedByAuthor': 'Revisions submitted by author', + 'dashboard.stage.copyediting': 'Copyediting', + 'dashboard.stage.deskReview': 'Desk Review', + 'dashboard.stage.production': 'Production', + 'dashboard.stage.published': 'Published', + 'dashboard.stage.reviewWithRound': 'Review (Round {$round})', + 'dashboard.stage.scheduledForPublication': 'Scheduled For Publication', + 'dashboard.startNewSubmission': 'Start A New Submission', + 'dashboard.submitRevisions': 'Submit revisions', + 'dashboard.summary.acceptAndSkipReview': 'Accept and skip review', + 'dashboard.summary.acceptReview': 'Accept review', + 'dashboard.summary.acceptSubmission': 'Accept Submission', + 'dashboard.summary.accessReviewForm': 'Access review form', + 'dashboard.summary.assignToIssue': 'Assign To Issue', + 'dashboard.summary.cancelReviewRound': 'Cancel Review Round', + 'dashboard.summary.copyeditedFiles': 'Copyedited Files', + 'dashboard.summary.daysInCopyediting': 'Days in copyediting', + 'dashboard.summary.daysInProduction': 'Days in production', + 'dashboard.summary.daysInReview': 'Days in review', + 'dashboard.summary.daysInSubmission': 'Days in submission', + 'dashboard.summary.daysSinceSubmission': 'Days since submission', + 'dashboard.summary.decline': 'Decline', + 'dashboard.summary.declineSubmission': 'Decline Submission', + 'dashboard.summary.deskReviewFiles': 'Desk Review Files', + 'dashboard.summary.deskReviewFilesDescription': + 'These are the files that will be taken forward to the review stage in the workflow.', + 'dashboard.summary.draftFiles': 'Draft Files', + 'dashboard.summary.draftFilesDescription': + 'These are files from the review stage which are to be copyedited', + 'dashboard.summary.editorsAssigned': 'Editors assigned', + 'dashboard.summary.filesForReview': 'Files for review', + 'dashboard.summary.filesForReviewDescription': + 'These files will be sent to the reviewers to review', + 'dashboard.summary.galleys': 'Galleys', + 'dashboard.summary.issueNo': 'Issue No', + 'dashboard.summary.journalName': 'Journal name', + 'dashboard.summary.notAssigned': 'Not assigned', + 'dashboard.summary.preview': 'Preview', + 'dashboard.summary.productionReadyFiles': 'Production Ready Files', + 'dashboard.summary.productionReadyFilesDescription': + 'These are the files that will be sent for publication', + 'dashboard.summary.requestRevisions': 'Request Revisions', + 'dashboard.summary.reviewer': 'Reviewer', + 'dashboard.summary.reviewerStatus': 'Reviewer status', + 'dashboard.summary.reviewers': 'Reviewers', + 'dashboard.summary.revisionsSubmitted': 'Revisions Submitted', + 'dashboard.summary.revisionsSubmittedDescription': + 'These files have been submitted by the author after visions were requested', + 'dashboard.summary.scheduleForProduction': 'Schedule for Publication', + 'dashboard.summary.sendSubmissionForReview': 'Send submission for review', + 'dashboard.summary.sendToProduction': 'Send to Production', + 'dashboard.summary.submissionLanguage': 'Submission Language', + 'dashboard.summary.submittedOn': 'Submitted on', + 'dashboard.summary.unschedule': 'Unschedule', + 'dashboard.summary.uploadFile': 'Upload File', + 'dashboard.summary.uploadRevisions': 'Upload Revisions', + 'dashboard.summary.viewActivityLog': 'View activity log', + 'dashboard.summary.viewSubmissionInDetail': 'View submission in detail', + 'dashboard.viewSummary': 'Summary', 'doi.manager.versions.countStatement': 'There are {$count} versions.', 'doi.manager.versions.modalTitle': 'DOIs for all versions', 'doi.manager.versions.view': 'View all', + 'editor.review.cancelReviewer': 'Cancel Reviewer', + 'editor.review.resendRequestReviewer': 'Resend Review Request', + 'editor.review.reviewDetails': 'Review Details', + 'editor.review.unassignReviewer': 'Unassign Reviewer', + 'editor.submission.addStageParticipant': 'Assign Participant', + 'editor.submission.decision.requestRevisions': 'Request Revisions', + 'editor.submission.schedulePublication': 'Schedule For Publication', 'editor.submission.search': 'Search submissions, ID, authors, keywords, etc.', - 'editor.submission.viewSummary': 'Summary', 'editor.submissionArchive.confirmDelete': 'Are you sure you want to permanently delete this submission?', + 'editor.submissionReview.editReview': 'Edit Review', + 'editor.submissionReview.uploadAttachment': 'Upload File', + 'editor.submissionReview.uploadFile': 'Upload Review File', + 'email.bcc': 'BCC', + 'email.cc': 'CC', + 'email.confirmSwitchLocale': + 'Are you sure you want to change to {$localeName} to compose this email? Any changes you have made to the subject and body of the email will be lost.', + 'email.subject': 'Subject', + 'email.to': 'To', 'form.dataHasChanged': 'The data on this form has changed. Do you wish to continue without saving?', 'form.errorA11y': 'Go to {$fieldLabel}: {$errorMessage}', @@ -182,6 +341,15 @@ window.pkp = { 'form.saved': 'Saved', 'grid.action.sort': 'Sort', 'help.help': 'Help', + 'invitation.orcid.message': '##invitation.orcid.message##', + 'invitation.role.addRole.button': '##invitation.role.addRole.button##', + 'invitation.role.dateEnd': '##invitation.role.dateEnd##', + 'invitation.role.dateStart': '##invitation.role.dateStart##', + 'invitation.role.masthead': '##invitation.role.masthead##', + 'invitation.role.removeRole.button': + '##invitation.role.removeRole.button##', + 'invitation.role.selectRole': '##invitation.role.selectRole##', + 'invitation.wizard.completeSteps': '##invitation.wizard.completeSteps##', 'issue.issue': 'Issue', 'list.collapseAll': 'Collapse all', 'list.expandAll': 'Expand all', @@ -238,6 +406,8 @@ window.pkp = { 'manager.dois.registration.viewError': 'View Error', 'manager.dois.registration.viewError.title': 'Registration Error Message', 'manager.dois.registration.viewRecord': 'View Record', + 'manager.dois.registration.viewRecord.title': + 'Successful Registration Message', 'manager.dois.status.error': 'Error', 'manager.dois.status.error.description': 'All items that have encountered an error in the registration process.', @@ -260,8 +430,9 @@ window.pkp = { 'manager.dois.update.failedCreation': 'DOI Updates Failed', 'manager.dois.update.partialFailure': 'Some DOI(s) could not be updated', 'manager.dois.update.success': 'DOI(s) successfully updated', + 'metadata.property.displayName.doi': 'DOI', 'navigation.backTo': '\u27f5 Back to {$page}', - 'navigation.submissions': 'Submissions', + 'publication.contributors': 'Contributors', 'publication.jats.autoCreatedMessage': 'This JATS file is generated automatically by the submission metadata', 'publication.jats.confirmDeleteFileButton': 'Delete JATS File', @@ -270,11 +441,22 @@ window.pkp = { 'publication.jats.confirmDeleteFileTitle': 'Confirm deleting JATS XML', 'publication.jats.lastModified': 'Last Modification at {$modificationDate} by {$username}', + 'publication.selectIssue': 'Select an issue to schedule for publication', 'publication.status.published': 'Published', 'publication.status.unpublished': 'Unpublished', + 'publication.unschedule': 'Unschedule', + 'publication.unschedule.confirm': + "Are you sure you don't want this scheduled for publication?", 'publication.version': 'Version {$version}', + 'reviewer.article.decision.accept': 'Accept Submission', + 'reviewer.article.decision.decline': 'Decline Submission', + 'reviewer.article.decision.pendingRevisions': 'Revisions Required', + 'reviewer.article.decision.resubmitElsewhere': 'Resubmit Elsewhere', + 'reviewer.article.decision.resubmitHere': 'Resubmit for Review', + 'reviewer.article.decision.seeComments': 'See Comments', 'reviewer.article.recommendation': 'Recommendation', 'reviewer.submission.acceptedOn': 'Review Accepted On', + 'reviewer.submission.declineReview': 'Decline Review Request', 'reviewer.submission.responseDueDate': 'Response Due Date', 'reviewer.submission.reviewDueDate': 'Review Due Date', 'reviewer.submission.reviewRequestDate': "Editor's Request", @@ -289,8 +471,6 @@ window.pkp = { 'reviewer.submission.reviewRound.emailLog': 'Decline reason sent by email', 'reviewer.submission.reviewRound.emailLog.defaultMessage': 'No reason given to the decline of the review invitation.', - 'reviewer.submission.reviewRound.reviewNotCompleted': - 'The review was not completed.', 'reviewer.submission.reviewRound.files': 'Files For Review', 'reviewer.submission.reviewRound.files.description': 'These files were sent to you for review', @@ -306,7 +486,11 @@ window.pkp = { 'reviewer.submission.reviewRound.metadata.keywords': 'Keywords', 'reviewer.submission.reviewRound.metadata.type': 'Type', 'reviewer.submission.reviewRound.reviewDeclineDate': 'Declined Date', + 'reviewer.submission.reviewRound.reviewNotCompleted': + 'The review was not completed.', 'reviewer.submission.submittedOn': 'Review Submitted On', + 'search.searchResults': 'Search Results', + 'stageParticipants.notify.message': 'Message', 'stats.countWithYearlyAverage': '{$count} ({$average}/year)', 'stats.descriptionForStat': 'Description for {$stat}', 'submission.list.assignEditor': 'Assign Editor', @@ -320,52 +504,6 @@ window.pkp = { 'submission.list.infoCenter': 'Activity Log & Notes', 'submission.list.responseDue': 'Response Due: {$date}', 'submission.list.reviewAssignment': 'Review Assignment', - 'submission.list.reviewAssignment.action.cancelReviewer': 'Cancel Reviewer', - 'submission.list.reviewAssignment.action.editDueDate': 'Edit Due Date', - 'submission.list.reviewAssignment.action.resendReviewRequest': - 'Resend Review Request', - 'submission.list.reviewAssignment.action.unassignReviewer': 'Unassign', - 'submission.list.reviewAssignment.action.viewDetails': 'View details', - 'submission.list.reviewAssignment.action.viewRecommendation': - 'View recommendation', - 'submission.list.reviewAssignment.action.viewUnreadRecommendation': - 'View unread recommendation', - 'submission.list.reviewAssignment.statusAccepted.description': - 'This reviewer has accepted the review request. Their review is due in {$days} days on {$date}.', - 'submission.list.reviewAssignment.statusAccepted.title': - 'Ongoing review - request accepted', - 'submission.list.reviewAssignment.statusAwaitingResponse.description': - 'Review request has been shared with Reviewer. Response is awaited in {$days} days on {$date}', - 'submission.list.reviewAssignment.statusAwaitingResponse.title': - 'Awaiting Response from the Reviewer', - 'submission.list.reviewAssignment.statusCancelled.description': - 'Reviewer has cancelled the review request on {$date}', - 'submission.list.reviewAssignment.statusCancelled.title': - 'Reviewer cancelled review request', - 'submission.list.reviewAssignment.statusComplete.description': - 'The review was accepted by the editor on {$date}.', - 'submission.list.reviewAssignment.statusComplete.title': - 'Review was confirmed by editor', - 'submission.list.reviewAssignment.statusDeclined.description': - 'Reviewer declined the review request on {$date}', - 'submission.list.reviewAssignment.statusDeclined.title': - 'Review Request declined on {$date}', - 'submission.list.reviewAssignment.statusReceived.description': - 'The review was completed on {$date} with the following recommendation: {$recommendation}', - 'submission.list.reviewAssignment.statusReceived.title': - 'Review completed on {$date}', - 'submission.list.reviewAssignment.statusRequestResend.description': - 'Review request has been reshared with reviewer. Response is awaited in {$days} days on {$date}', - 'submission.list.reviewAssignment.statusRequestResend.title': - 'Awaiting Response from the Reviewer', - 'submission.list.reviewAssignment.statusResponseOverdue.description': - 'This reviewer has not responded to the review request. A response was due on {$date}', - 'submission.list.reviewAssignment.statusResponseOverdue.title': - 'Review Request overdue by {$days} days', - 'submission.list.reviewAssignment.statusReviewOverdue.description': - 'This reviewer has not completed their review. A response was due on {$date}.', - 'submission.list.reviewAssignment.statusReviewOverdue.title': - 'Review overdue by {$days} days', 'submission.list.reviewCancelled': 'Review Cancelled', 'submission.list.reviewComplete': 'Review Submitted', 'submission.list.reviewDue': 'Review Due: {$date}', @@ -373,26 +511,15 @@ window.pkp = { 'You have been assigned an editorial role for this submission. Would you like to access the Editorial workflow?', 'submission.list.reviewsCompleted': 'Assigned reviews completed', 'submission.list.revisionsSubmitted': 'Revisions submitted', - 'submission.round': 'Round {$round}', 'submission.submit.newSubmissionSingle': 'New Submission', 'submission.upload.percentComplete': 'Uploading {$percent}% complete', + 'submissions.declined': 'Declined', 'submissions.incomplete': 'Incomplete', - 'validator.required': 'This field is required.', - 'invitation.notification.title': 'Invitation sent', - 'invitation.wizard.success': "{$email} has been invited to a new role in OJS. You can be updated about the user's decision on the User & Role page, your OJS notification and/or your email", + todo: '##todo##', 'user.email': 'Email', + 'user.orcid': 'ORCID iD', 'user.username': 'Username', - 'user.orcid': 'ORCiD ID', - 'invitation.notification.closeBtn':'View all users', - 'user.password': 'Password', - 'invitation.role.selectRole':'Select a new role', - 'invitation.role.dateEnd' : 'End Date', - 'invitation.role.dateStart' : 'Start Date', - 'invitation.role.masthead' : 'Journal Masthead', - 'invitation.role.removeRole.button' : 'Remove Role', - 'invitation.role.addRole.button':'Add Another Role', - 'invitation.orcid.message':'Add Another Role', - + 'validator.required': 'This field is required.', }, tinyMCE: { diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index 606f1f07d..000000000 --- a/src/App.vue +++ /dev/null @@ -1,489 +0,0 @@ - - - - - diff --git a/src/components/Container/DecisionPage.vue b/src/components/Container/DecisionPage.vue index a563e9138..be9b2c579 100644 --- a/src/components/Container/DecisionPage.vue +++ b/src/components/Container/DecisionPage.vue @@ -59,6 +59,8 @@ export default { steps: [], /** The URL to the editorial workflow of the submission. */ submissionUrl: '', + /** The URL to the submission summary in dashboard. */ + submissionSummaryUrl: '', /** The URL to the submission in the REST API. */ submissionApiUrl: '', /** The URL to the current user's submissions list. */ @@ -110,6 +112,22 @@ export default { isOnLastStep() { return this.currentStepIndex === this.steps.length - 1; }, + + /** + * Indicate the decision was triggered from submission summary page + * or from workflow page + */ + returnUrlToSubmissionSummary() { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const ret = urlParams.get('ret'); + if (ret) { + let returnUrl = decodeURIComponent(ret); + return `${pkp?.context?.pageBaseUrl}${returnUrl}`; + } + + return null; + }, }, created() { if (this.steps.length) { @@ -143,7 +161,10 @@ export default { * decision and return to the submission */ cancel() { - console.log('click on cancel'); + const submissionUrl = this.returnUrlToSubmissionSummary + ? this.returnUrlToSubmissionSummary + : this.submissionUrl; + this.openDialog({ name: 'cancel', title: this.abandonDecisionLabel, @@ -153,7 +174,7 @@ export default { label: this.abandonDecisionLabel, isWarnable: true, callback: () => { - window.location = this.submissionUrl; + window.location = submissionUrl; }, }, { @@ -208,11 +229,22 @@ export default { * Open the modal when decision is complete */ openCompletedDialog() { - this.openDialog({ - name: 'completed', - title: this.decisionCompleteLabel, - message: this.decisionCompleteDescription, - actions: [ + let actions; + let close; + + if (this.returnUrlToSubmissionSummary) { + actions = [ + { + label: this.viewSubmissionSummaryLabel, + element: 'a', + href: this.returnUrlToSubmissionSummary, + }, + ]; + close = () => { + window.location = this.returnUrlToSubmissionSummary; + }; + } else { + actions = [ { label: this.viewSubmissionLabel, element: 'a', @@ -223,10 +255,17 @@ export default { element: 'a', href: this.submissionListUrl, }, - ], - close: () => { + ]; + close = () => { window.location = this.submissionUrl; - }, + }; + } + this.openDialog({ + name: 'completed', + title: this.decisionCompleteLabel, + message: this.decisionCompleteDescription, + actions, + close, }); }, @@ -331,12 +370,12 @@ export default { bcc: bcc ? bcc.split(',').map((item) => { return item.trim(); - }) + }) : [], cc: cc ? cc.split(',').map((item) => { return item.trim(); - }) + }) : [], locale: step.locale, recipients: step.canChangeRecipients ? step.recipients : [], diff --git a/src/components/Container/PageOJS.vue b/src/components/Container/PageOJS.vue index dd798b54e..8ffc47147 100644 --- a/src/components/Container/PageOJS.vue +++ b/src/components/Container/PageOJS.vue @@ -1,11 +1,11 @@ - - diff --git a/src/components/Icon/Icon.stories.js b/src/components/Icon/Icon.stories.js index 471e370a4..b0a2511ee 100644 --- a/src/components/Icon/Icon.stories.js +++ b/src/components/Icon/Icon.stories.js @@ -74,6 +74,7 @@ export const iconGallery = { 'Email', 'EmailOpened', 'Expand', + 'DefaultDocument', 'FileAudio', 'FileDoc', 'FileEpub', diff --git a/src/components/Icon/Icon.vue b/src/components/Icon/Icon.vue index c84523de5..8cb5b290a 100644 --- a/src/components/Icon/Icon.vue +++ b/src/components/Icon/Icon.vue @@ -52,6 +52,7 @@ import Edit from './icons/Edit.vue'; import Email from './icons/Email.vue'; import EmailOpened from './icons/EmailOpened.vue'; import Expand from './icons/Expand.vue'; +import DefaultDocument from './icons/DefaultDocument.vue'; import FileAudio from './icons/FileAudio.vue'; import FileDoc from './icons/FileDoc.vue'; import FileEpub from './icons/FileEpub.vue'; @@ -99,6 +100,7 @@ const svgIcons = { Cancel, Complete, Dashboard, + DefaultDocument, DecreaseTextSize, DisableUser, Dropdown, diff --git a/src/components/Icon/icons/DefaultDocument.vue b/src/components/Icon/icons/DefaultDocument.vue new file mode 100644 index 000000000..ca84b6130 --- /dev/null +++ b/src/components/Icon/icons/DefaultDocument.vue @@ -0,0 +1,15 @@ + diff --git a/src/components/Modal/AjaxModalWrapper.vue b/src/components/Modal/AjaxModalWrapper.vue index 9299ccea0..ac0999c33 100644 --- a/src/components/Modal/AjaxModalWrapper.vue +++ b/src/components/Modal/AjaxModalWrapper.vue @@ -20,6 +20,8 @@ const {legacyOptions} = defineProps({ }, }); +const closeModal = inject('closeModal'); + const contentDiv = ref(null); // eslint-disable-next-line no-unused-vars const pkp = window.pkp; @@ -68,10 +70,43 @@ function passToHandlerElement(...args) { if (legacyOptions.modalHandler) { legacyOptions.modalHandler.getHtmlElement().trigger(...args); } + // when legacy modal opened from vue.js, it does not have handler + // and needs to trigger close for some events + else { + const eventType = args?.[0]?.type; + + if ( + [ + 'formSubmitted', + 'formCanceled', + 'ajaxHtmlError', + 'modalFinished', + 'wizardClose', + 'wizardCancel', + ].includes(eventType) + ) { + closeModal(); + } + } return; } +function onVueFormSuccess(formId) { + if ( + legacyOptions.closeOnFormSuccessId && + legacyOptions.closeOnFormSuccessId === formId + ) { + setTimeout(function () { + if (legacyOptions.modalHandler) { + legacyOptions.modalHandler.modalClose(); + } else { + closeModal(); + } + }, 1000); + } +} + onMounted(async () => { await fetchAssignParticipantPage(); if (modalData.value) { @@ -92,6 +127,9 @@ onMounted(async () => { $(contentDiv.value).bind('dataChanged', passToHandlerElement); $(contentDiv.value).bind('updateHeader', passToHandlerElement); $(contentDiv.value).bind('gridRefreshRequested', passToHandlerElement); + + // to handle Vue.js form global form-success event, mimicking behavior from ModalHandler.js + pkp.eventBus.$on('form-success', onVueFormSuccess); } }); @@ -108,5 +146,6 @@ onBeforeUnmount(() => { $(contentDiv.value).unbind('dataChanged', passToHandlerElement); $(contentDiv.value).unbind('updateHeader', passToHandlerElement); $(contentDiv.value).unbind('gridRefreshRequested', passToHandlerElement); + pkp.eventBus.$off('form-success', onVueFormSuccess); }); diff --git a/src/components/Modal/Dialog.vue b/src/components/Modal/Dialog.vue index 8246acda6..dbf71b3e2 100644 --- a/src/components/Modal/Dialog.vue +++ b/src/components/Modal/Dialog.vue @@ -117,6 +117,9 @@ watch(opened, (prevOpened, nextOpened) => { }); function onClose() { + if (dialogProps.value.close) { + dialogProps.value.close(); + } closeDialog(); } diff --git a/src/components/Modal/SideModalBody.vue b/src/components/Modal/SideModalBody.vue index 302610d50..58785767e 100644 --- a/src/components/Modal/SideModalBody.vue +++ b/src/components/Modal/SideModalBody.vue @@ -38,7 +38,7 @@ -
+
diff --git a/src/components/ReviewActivityIndicatorPopover/ReviewActivityIndicatorPopover.vue b/src/components/ReviewActivityIndicatorPopover/ReviewActivityIndicatorPopover.vue deleted file mode 100644 index ab62a66a9..000000000 --- a/src/components/ReviewActivityIndicatorPopover/ReviewActivityIndicatorPopover.vue +++ /dev/null @@ -1,531 +0,0 @@ - - - diff --git a/src/components/StageBubble/StageBubble.vue b/src/components/StageBubble/StageBubble.vue index 7b54b1ef0..0b965c221 100644 --- a/src/components/StageBubble/StageBubble.vue +++ b/src/components/StageBubble/StageBubble.vue @@ -1,74 +1,41 @@ - + +}); - +const stageColorClass = computed( + () => ExtendedStagesColorClass[props.extendedStage], +); + diff --git a/src/components/UserAvatar/UserAvatar.mdx b/src/components/UserAvatar/UserAvatar.mdx new file mode 100644 index 000000000..806818130 --- /dev/null +++ b/src/components/UserAvatar/UserAvatar.mdx @@ -0,0 +1,11 @@ +import {Primary, Controls, Stories, Meta, Description} from '@storybook/blocks'; + +import * as UserAvatarStories from './UserAvatar.stories.js'; + + + +# UserAvatar + + + + diff --git a/src/components/UserAvatar/UserAvatar.stories.js b/src/components/UserAvatar/UserAvatar.stories.js new file mode 100644 index 000000000..27052a967 --- /dev/null +++ b/src/components/UserAvatar/UserAvatar.stories.js @@ -0,0 +1,27 @@ +import UserAvatar from './UserAvatar.vue'; + +export default { + title: 'Components/UserAvatar', + component: UserAvatar, + render: (args) => ({ + components: {UserAvatar}, + setup() { + return {args}; + }, + template: '', + }), +}; + +export const Default = { + args: { + userFullName: 'Nama Sampan-Nirmal Lengkap', + userId: 136, + }, +}; + +export const Arabic = { + args: { + userFullName: 'خالد محمود الفارسي', + userId: 136, + }, +}; diff --git a/src/components/UserAvatar/UserAvatar.vue b/src/components/UserAvatar/UserAvatar.vue new file mode 100644 index 000000000..a93509478 --- /dev/null +++ b/src/components/UserAvatar/UserAvatar.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/composables/useUser.js b/src/composables/useCurrentUser.js similarity index 90% rename from src/composables/useUser.js rename to src/composables/useCurrentUser.js index 1b0d81a73..ed392c1d1 100644 --- a/src/composables/useUser.js +++ b/src/composables/useCurrentUser.js @@ -1,6 +1,6 @@ import {computed} from 'vue'; -export function useUser() { +export function useCurrentUser() { const isSiteAdmin = computed( () => !!pkp.currentUser.roles.find( diff --git a/src/composables/useDataChanged.js b/src/composables/useDataChanged.js new file mode 100644 index 000000000..4f59b5ba5 --- /dev/null +++ b/src/composables/useDataChanged.js @@ -0,0 +1,11 @@ +export function useDataChanged() { + const callbacks = []; + function registerDataChangeCallback(callback) { + callbacks.push(callback); + } + function triggerDataChange() { + callbacks.forEach((callback) => callback()); + } + + return {registerDataChangeCallback, triggerDataChange}; +} diff --git a/src/composables/useDate.js b/src/composables/useDate.js new file mode 100644 index 000000000..a9b6975ff --- /dev/null +++ b/src/composables/useDate.js @@ -0,0 +1,19 @@ +import moment from 'moment'; + +export function useDate() { + function calculateDaysBetweenDates(startDate, endDate) { + const oneDay = 1000 * 60 * 60 * 24; // milliseconds in one day + const start = new Date(startDate); + const end = new Date(endDate); + + const difference = end - start; // difference in milliseconds + + return Math.round(difference / oneDay); + } + + function formatShortDate(dateString) { + return moment(dateString).format('DD-MM-YYYY'); + } + + return {calculateDaysBetweenDates, formatShortDate}; +} diff --git a/src/composables/useFiltersForm.js b/src/composables/useFiltersForm.js index 82ba31dd8..8eb101db8 100644 --- a/src/composables/useFiltersForm.js +++ b/src/composables/useFiltersForm.js @@ -26,7 +26,7 @@ function createSelected(values, labels) { export function useFiltersForm(_filtersForm) { const filtersForm = ref(_filtersForm); - const {clearForm, setValue} = useForm(_filtersForm); + const {clearForm, removeFieldValue, setValue} = useForm(_filtersForm); const filtersFormList = computed(() => { const list = []; @@ -42,6 +42,7 @@ export function useFiltersForm(_filtersForm) { ); list.push({ + name: field.name, fieldLabel: field.label, label: select.label, value: select.value, @@ -52,6 +53,7 @@ export function useFiltersForm(_filtersForm) { ); list.push({ + name: field.name, fieldLabel: field.label, label: option.label, value: option.value, @@ -59,6 +61,7 @@ export function useFiltersForm(_filtersForm) { } else if (field.component === 'field-slider') { if (fieldValue !== field.min) { list.push({ + name: field.name, fieldLabel: field.label, value: fieldValue, label: fieldValue, @@ -66,6 +69,7 @@ export function useFiltersForm(_filtersForm) { } } else { list.push({ + name: field.name, fieldLabel: field.label, value: fieldValue, label: 'TODO', @@ -124,7 +128,11 @@ export function useFiltersForm(_filtersForm) { } function clearFiltersForm() { - clearForm(filtersForm.value); + clearForm(); + } + + function clearFiltersFormField(fieldName, fieldValue) { + removeFieldValue(fieldName, fieldValue); } return { @@ -135,5 +143,6 @@ export function useFiltersForm(_filtersForm) { initFiltersFormFromQueryParams, updateFiltersForm, clearFiltersForm, + clearFiltersFormField, }; } diff --git a/src/composables/useForm.js b/src/composables/useForm.js index 8aba64950..f705279eb 100644 --- a/src/composables/useForm.js +++ b/src/composables/useForm.js @@ -99,20 +99,36 @@ export function useForm(_form) { } } + function clearFormField(fieldName) { + const field = getField(form.value, fieldName); + + if (field.isMultilingual) { + const newValueMultilingual = {}; + form.value.supportedFormLocales.forEach((localeObject) => { + const localeKey = localeObject.key; + const newValue = getClearValue(field, localeKey); + newValueMultilingual[localeKey] = newValue; + }); + setValue(field.name, newValueMultilingual); + } else { + const newValue = getClearValue(field); + setValue(field.name, newValue); + } + } + + function removeFieldValue(fieldName, fieldValue) { + const value = getValue(fieldName); + if (Array.isArray(value)) { + const newValue = value.filter((v) => v !== fieldValue); + setValue(fieldName, newValue); + } else { + clearFormField(fieldName); + } + } + function clearForm() { form.value.fields.forEach((field) => { - if (field.isMultilingual) { - const newValueMultilingual = {}; - form.value.supportedFormLocales.forEach((localeObject) => { - const localeKey = localeObject.key; - const newValue = getClearValue(field, localeKey); - newValueMultilingual[localeKey] = newValue; - }); - setValue(field.name, newValueMultilingual); - } else { - const newValue = getClearValue(field); - setValue(field.name, newValue); - } + clearFormField(field.name); }); } @@ -120,6 +136,7 @@ export function useForm(_form) { set, setValue, getValue, + removeFieldValue, clearForm, form, connectWithPayload, diff --git a/src/composables/useId.js b/src/composables/useId.js new file mode 100644 index 000000000..454256530 --- /dev/null +++ b/src/composables/useId.js @@ -0,0 +1,8 @@ +let id = 0; +function generateId(componentName) { + return `pkp-id-${++id}`; +} + +export function useId() { + return {generateId}; +} diff --git a/src/composables/useLegacyGridUrl.js b/src/composables/useLegacyGridUrl.js new file mode 100644 index 000000000..ee51f8c4f --- /dev/null +++ b/src/composables/useLegacyGridUrl.js @@ -0,0 +1,38 @@ +import {ref, computed} from 'vue'; + +function camelCaseToDashes(str) { + return str.replace(/([a-z]+)([A-Z])/g, '$1-$2').toLowerCase(); +} +export function useLegacyGridUrl({ + component: _component, + op: _op, + params: _params, +} = {}) { + if (typeof pkp === 'undefined' || !pkp?.context?.legacyGridBaseUrl) { + throw new Error('pkp.context.legacyGridBaseUrl is not configured'); + } + + const component = ref(_component); + const op = ref(_op); + const params = ref(_params); + + const queryParamsString = computed(() => { + if (params.value && Object.keys(params.value).length) { + return `?${new URLSearchParams(params.value).toString()}`; + } + return ''; + }); + + const url = computed(() => { + let componentPath = component.value.slice(0, -7); + componentPath = camelCaseToDashes(componentPath.split('.').join('/')); + const opPath = camelCaseToDashes(op.value); + + let baseUrl = pkp.context.legacyGridBaseUrl + .replace('component', componentPath) + .replace('action', opPath); + return `${baseUrl}${queryParamsString.value}`; + }); + + return {url}; +} diff --git a/src/composables/useLegacyGridUrl.test.js b/src/composables/useLegacyGridUrl.test.js new file mode 100644 index 000000000..5b347c05e --- /dev/null +++ b/src/composables/useLegacyGridUrl.test.js @@ -0,0 +1,47 @@ +import {expect, test, describe} from 'vitest'; +import {useLegacyGridUrl} from './useLegacyGridUrl'; + +global.pkp = global.pkp || {}; +global.pkp.context = { + legacyGridBaseUrl: + 'http://mock/index.php/publicknowledge/$$$call$$$/component/action', +}; + +describe('useLegacyGridUrl', () => { + test('grid.users.stageParticipant.StageParticipantGridHandler', () => { + // http://localhost:7003/index.php/publicknowledge/$$$call$$$/grid/users/stage-participant/stage-participant-grid/add-participant?submissionId=13&stageId=3 + const {url} = useLegacyGridUrl({ + component: 'grid.users.stageParticipant.StageParticipantGridHandler', + op: 'addParticipant', + params: {submissionId: 13, stageId: 3}, + }); + + expect(url.value).toBe( + 'http://mock/index.php/publicknowledge/$$$call$$$/grid/users/stage-participant/stage-participant-grid/add-participant?submissionId=13&stageId=3', + ); + }); + + test('grid.users.reviewer.ReviewerGridHandler', () => { + const {url} = useLegacyGridUrl({ + component: 'grid.users.reviewer.ReviewerGridHandler', + op: 'readReview', + params: {submissionId: 13, reviewAssignmentId: 19, stageId: 3}, + }); + + expect(url.value).toBe( + 'http://mock/index.php/publicknowledge/$$$call$$$/grid/users/reviewer/reviewer-grid/read-review?submissionId=13&reviewAssignmentId=19&stageId=3', + ); + }); + + test('modals.publish.AssignToIssueHandler', () => { + const {url} = useLegacyGridUrl({ + component: 'modals.publish.AssignToIssueHandler', + op: 'assign', + params: {submissionId: 13, publicationId: 14}, + }); + + expect(url.value).toBe( + 'http://mock/index.php/publicknowledge/$$$call$$$/modals/publish/assign-to-issue/assign?submissionId=13&publicationId=14', + ); + }); +}); diff --git a/src/composables/useModal.js b/src/composables/useModal.js index 2fb67e2b5..548a1b54b 100644 --- a/src/composables/useModal.js +++ b/src/composables/useModal.js @@ -7,8 +7,8 @@ export function useModal() { modalStore.openDialog(props); } - function openSideModal(component, props) { - modalStore.openSideModal(component, props); + function openSideModal(component, props, opts) { + modalStore.openSideModal(component, props, opts); } return {openDialog, openSideModal}; diff --git a/src/composables/useParticipant.js b/src/composables/useParticipant.js new file mode 100644 index 000000000..bd1783310 --- /dev/null +++ b/src/composables/useParticipant.js @@ -0,0 +1,37 @@ +export function useParticipant() { + function getEditorRoleIds() { + return [ + pkp.const.ROLE_ID_MANAGER, + pkp.const.ROLE_ID_SUB_EDITOR, + pkp.const.ROLE_ID_ASSISTANT, + ]; + } + function hasParticipantAtLeastOneRole(participant, roleIds = []) { + return participant.groups.some((group) => roleIds.includes(group.roleId)); + } + + function getFirstGroupWithFollowingRoles(participant, roleIds = []) { + return participant.groups.find((group) => roleIds.includes(group.roleId)); + } + + function getUserAvatarInitialsFromName(fullName) { + const fullNameParts = fullName.split(' '); + + return fullNameParts + .map((part) => { + const partTrimmed = part.trim(); + if (partTrimmed.length) { + return partTrimmed[0].toUpperCase(); + } + return ''; + }) + .join('') + .substring(0, 3); + } + return { + getUserAvatarInitialsFromName, + getEditorRoleIds, + hasParticipantAtLeastOneRole, + getFirstGroupWithFollowingRoles, + }; +} diff --git a/src/composables/useParticipant.test.js b/src/composables/useParticipant.test.js new file mode 100644 index 000000000..745547f6b --- /dev/null +++ b/src/composables/useParticipant.test.js @@ -0,0 +1,29 @@ +import {expect, test, describe} from 'vitest'; +import {useParticipant} from './useParticipant'; + +describe('useParticipant', () => { + describe('getUserAvatarInitials', () => { + const {getUserAvatarInitialsFromName} = useParticipant(); + test('Two names', () => { + expect(getUserAvatarInitialsFromName('Charlotte Reynolds')).toBe('CR'); + }); + + test('Three names with dash', () => { + expect(getUserAvatarInitialsFromName('Nama Sampan-Nirmal Lengkap')).toBe( + 'NSL', + ); + }); + + test('Three names', () => { + expect(getUserAvatarInitialsFromName('Theresa Jessie Franklin')).toBe( + 'TJF', + ); + }); + + test('Four names (max 3 initials)', () => { + expect( + getUserAvatarInitialsFromName('Theresa Jessie Franklin Jasmin'), + ).toBe('TJF'); + }); + }); +}); diff --git a/src/composables/useReviewAssignment.js b/src/composables/useReviewAssignment.js new file mode 100644 index 000000000..3674aee44 --- /dev/null +++ b/src/composables/useReviewAssignment.js @@ -0,0 +1,61 @@ +const InProgressReviewAssignmentStatuses = [ + pkp.const.REVIEW_ASSIGNMENT_STATUS_ACCEPTED, + pkp.const.REVIEW_ASSIGNMENT_STATUS_REVIEW_OVERDUE, +]; +const CompletedReviewAssignmentStatuses = [ + pkp.const.REVIEW_ASSIGNMENT_STATUS_RECEIVED, + pkp.const.REVIEW_ASSIGNMENT_STATUS_COMPLETE, + pkp.const.REVIEW_ASSIGNMENT_STATUS_THANKED, + pkp.const.REVIEW_ASSIGNMENT_STATUS_CANCELLED, + pkp.const.REVIEW_ASSIGNMENT_STATUS_REQUEST_RESEND, +]; + +const IgnoredReviewAssignmentStatuses = [ + pkp.const.REVIEW_ASSIGNMENT_STATUS_DECLINED, + pkp.const.REVIEW_ASSIGNMENT_STATUS_CANCELLED, +]; + +export function useReviewAssignment() { + function getActiveReviewAssignments(reviewAssignments) { + return reviewAssignments.filter( + (reviewAssignment) => + !IgnoredReviewAssignmentStatuses.includes(reviewAssignment.statusId), + ); + } + + function getCompletedReviewAssignments(reviewAssignments = []) { + return getActiveReviewAssignments(reviewAssignments).filter( + (reviewAssignment) => + CompletedReviewAssignmentStatuses.includes(reviewAssignment.statusId), + ); + } + + function getOpenReviewAssignments(reviewAssignments = []) { + return reviewAssignments.filter( + (reviewAssignment) => + reviewAssignment.reviewMethod === + pkp.const.SUBMISSION_REVIEW_METHOD_OPEN, + ); + } + + function getReviewMethodIcons(reviewAssignment) { + switch (reviewAssignment.reviewMethod) { + case pkp.const.SUBMISSION_REVIEW_METHOD_ANONYMOUS: + return ['OpenReview', 'AnonymousReview']; + case pkp.const.SUBMISSION_REVIEW_METHOD_DOUBLEANONYMOUS: + return ['AnonymousReview', 'AnonymousReview']; + case pkp.const.SUBMISSION_REVIEW_METHOD_OPEN: + return ['OpenReview', 'OpenReview']; + } + + return ['OpenReview', 'OpenReview']; + } + + return { + getActiveReviewAssignments, + getCompletedReviewAssignments, + getOpenReviewAssignments, + getReviewMethodIcons, + InProgressReviewAssignmentStatuses, + }; +} diff --git a/src/composables/useSubmission.js b/src/composables/useSubmission.js new file mode 100644 index 000000000..9e962cda2 --- /dev/null +++ b/src/composables/useSubmission.js @@ -0,0 +1,112 @@ +import {useLocalize} from './useLocalize'; + +const {t, tk} = useLocalize(); + +export const ExtendedStages = { + INCOMPLETE: 'incomplete', + SUBMISSION: 'submission', + INTERNAL_REVIEW: 'internalReview', + EXTERNAL_REVIEW: 'externalReview', + EDITING: 'editing', + PRODUCTION_QUEUED: 'productionQueued', + PRODUCTION_SCHEDULED: 'productionScheduled', + PRODUCTION_PUBLISHED: 'productionPublished', + DECLINED: 'declined', +}; + +export const ExtendedStagesLabels = { + incomplete: tk('submissions.incomplete'), + submission: tk('dashboard.stage.deskReview'), + internalReview: tk('todo'), + externalReview: tk('dashboard.stage.reviewWithRound'), + editing: tk('dashboard.stage.copyediting'), + productionQueued: tk('dashboard.stage.production'), + productionScheduled: tk('dashboard.stage.scheduledForPublication'), + productionPublished: tk('dashboard.stage.published'), + declined: tk('submissions.declined'), +}; + +export function useSubmission() { + function getActiveStage(submission) { + return submission.stages.find((stage) => stage.isActiveStage); + } + + function getCurrentReviewRound(submission) { + return submission?.reviewRounds?.length + ? submission?.reviewRounds[submission.reviewRounds.length - 1] + : null; + } + + function getCurrentReviewAssignments(submission) { + const currentReviewRound = getCurrentReviewRound(submission); + + return submission.reviewAssignments.filter( + (reviewAssignment) => reviewAssignment.round === currentReviewRound.round, + ); + } + + function getCurrentPublication(submission) { + return submission.publications.find( + (publication) => publication.id === submission.currentPublicationId, + ); + } + + function getExtendedStage(submission) { + const activeStage = getActiveStage(submission); + + switch (activeStage.id) { + case pkp.const.WORKFLOW_STAGE_ID_SUBMISSION: + return submission.submissionProgress + ? ExtendedStages.INCOMPLETE + : ExtendedStages.SUBMISSION; + case pkp.const.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW: + return ExtendedStages.EXTERNAL_REVIEW; + case pkp.const.WORKFLOW_STAGE_ID_EDITING: + return ExtendedStages.EDITING; + case pkp.const.WORKFLOW_STAGE_ID_PRODUCTION: + switch (submission.status) { + case pkp.const.STATUS_QUEUED: + return ExtendedStages.PRODUCTION_QUEUED; + case pkp.const.STATUS_SCHEDULED: + return ExtendedStages.PRODUCTION_SCHEDULED; + case pkp.const.STATUS_PUBLISHED: + return ExtendedStages.PRODUCTION_PUBLISHED; + case pkp.const.STATUS_DECLINED: + return ExtendedStages.PRODUCTION_DECLINED; + } + } + } + + function getExtendedStageLabel(submission) { + const extendedStage = getExtendedStage(submission); + const round = + extendedStage === ExtendedStages.EXTERNAL_REVIEW + ? submission.reviewRounds[submission.reviewRounds.length - 1].round + : undefined; + return t(ExtendedStagesLabels[extendedStage], { + round, + }); + } + + function getFileStageFromWorkflowStage(submission) { + const FileStageMapping = { + [pkp.const.WORKFLOW_STAGE_ID_SUBMISSION]: + pkp.const.SUBMISSION_FILE_SUBMISSION, + [pkp.const.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW]: + pkp.const.SUBMISSION_FILE_REVIEW_REVISION, + [pkp.const.WORKFLOW_STAGE_ID_EDITING]: pkp.const.SUBMISSION_FILE_FINAL, + }; + + return FileStageMapping[submission.stageId]; + } + + return { + getActiveStage, + getExtendedStage, + getExtendedStageLabel, + getCurrentReviewRound, + getCurrentReviewAssignments, + getCurrentPublication, + getFileStageFromWorkflowStage, + }; +} diff --git a/src/composables/useUrl.js b/src/composables/useUrl.js index dd59cb260..d02feb95d 100644 --- a/src/composables/useUrl.js +++ b/src/composables/useUrl.js @@ -5,7 +5,7 @@ import {ref, computed} from 'vue'; * is covered in useFetch */ -export function useUrl(_path) { +export function useUrl(_path, _queryParams = {}) { if (typeof pkp === 'undefined' || !pkp?.context?.apiBaseUrl) { throw new Error('pkp.context.apiBaseUrl is not configured'); } @@ -16,9 +16,27 @@ export function useUrl(_path) { // normalise to be ref even if its not passed as ref const path = ref(_path); + const queryParams = ref(_queryParams); - const apiUrl = computed(() => `${pkp.context.apiBaseUrl}${path.value}`); - const pageUrl = computed(() => `${pkp.context.pageBaseUrl}${path.value}`); + const queryParamsString = computed(() => { + if (queryParams.value && Object.keys(queryParams.value).length) { + return `?${new URLSearchParams(queryParams.value).toString()}`; + } + return ''; + }); - return {apiUrl, pageUrl}; + const apiUrl = computed( + () => `${pkp.context.apiBaseUrl}${path.value}${queryParamsString.value}`, + ); + const pageUrl = computed(() => + path.value.startsWith('http') + ? `${path.value}${queryParamsString.value}` + : `${pkp.context.pageBaseUrl}${path.value}${queryParamsString.value}`, + ); + + function redirectToPage() { + window.location.href = pageUrl.value; + } + + return {apiUrl, pageUrl, redirectToPage}; } diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 1c0ed8706..000000000 --- a/src/main.js +++ /dev/null @@ -1,145 +0,0 @@ -import {createApp, h} from 'vue'; -import {createPinia} from 'pinia'; - -import emitter from 'tiny-emitter/instance'; - -//import './styles/style.css'; -import App from './App.vue'; - -import router from './router'; - -import GlobalMixins from '@/mixins/global.js'; -import VueAnnouncer from '@vue-a11y/announcer'; -import FloatingVue from 'floating-vue'; - -import VueScrollTo from 'vue-scrollto'; - -import Badge from '@/components/Badge/Badge.vue'; -import Dropdown from '@/components/Dropdown/Dropdown.vue'; -import Icon from '@/components/Icon/Icon.vue'; -import Notification from '@/components/Notification/Notification.vue'; -import Panel from '@/components/Panel/Panel.vue'; -import PanelSection from '@/components/Panel/PanelSection.vue'; -import PkpButton from '@/components/Button/Button.vue'; -import PkpHeader from '@/components/Header/Header.vue'; -import Spinner from '@/components/Spinner/Spinner.vue'; -import Step from '@/components/Steps/Step.vue'; -import Steps from '@/components/Steps/Steps.vue'; -import Tab from '@/components/Tabs/Tab.vue'; -import Tabs from '@/components/Tabs/Tabs.vue'; - -export default window.pkp.eventBus = { - $on: (...args) => emitter.on(...args), - $once: (...args) => emitter.once(...args), - $off: (...args) => emitter.off(...args), - $emit: (...args) => emitter.emit(...args), -}; - -const vueApp = createApp({ - data() { - /** - * Fake data that is passed to the root component - * - * This data is usually added to every PageComponent by the - * PKPTemplateManager class in OJS, OMP or OPS. - */ - - return { - /** - * File genres - */ - fileGenres: [ - { - id: 1, - name: 'Book Manuscript', - isPrimary: true, - }, - { - id: 2, - name: 'Chapter Manuscript', - isPrimary: true, - }, - { - id: 3, - name: 'Preface', - }, - { - id: 4, - name: 'Index', - }, - { - id: 5, - name: 'Glossary', - }, - { - id: 7, - name: 'Prospectus', - }, - { - id: 11, - name: 'Table', - }, - { - id: 8, - name: 'Figure', - }, - { - id: 9, - name: 'Audio', - }, - { - id: 10, - name: 'Other', - }, - ], - /** - * TinyMCE configuration - */ - tinyMCE: { - skinUrl: '/styles/tinymce', - }, - }; - }, - render: () => h(App), -}); - -const pinia = createPinia(); -vueApp.use(pinia); - -vueApp.config.productionTip = false; -vueApp.config.compilerOptions.whitespace = 'preserve'; - -vueApp.mixin(GlobalMixins); - -vueApp.component('Badge', Badge); -vueApp.component('Dropdown', Dropdown); -vueApp.component('Icon', Icon); -vueApp.component('Notification', Notification); -vueApp.component('Panel', Panel); -vueApp.component('PanelSection', PanelSection); -vueApp.component('PkpButton', PkpButton); -vueApp.component('PkpHeader', PkpHeader); -vueApp.component('Spinner', Spinner); -vueApp.component('Step', Step); -vueApp.component('Steps', Steps); -vueApp.component('Tab', Tab); -vueApp.component('Tabs', Tabs); - -vueApp.use(router); - -vueApp.use(VueScrollTo); -vueApp.use(VueAnnouncer); -vueApp.use(FloatingVue, { - themes: { - 'pkp-tooltip': { - $extend: 'tooltip', - triggers: ['click'], - delay: { - show: 0, - hide: 0, - }, - }, - }, -}); - -vueApp.mount('#app'); diff --git a/src/managers/ContributorManager/ContributorManager.vue b/src/managers/ContributorManager/ContributorManager.vue new file mode 100644 index 000000000..17ea0201d --- /dev/null +++ b/src/managers/ContributorManager/ContributorManager.vue @@ -0,0 +1,37 @@ + + diff --git a/src/managers/ContributorManager/contributorManagerStore.js b/src/managers/ContributorManager/contributorManagerStore.js new file mode 100644 index 000000000..d3f7ad95b --- /dev/null +++ b/src/managers/ContributorManager/contributorManagerStore.js @@ -0,0 +1,24 @@ +import {defineComponentStore} from '@/utils/defineComponentStore'; + +import {computed} from 'vue'; +import {useFetch} from '@/composables/useFetch'; +import {useUrl} from '@/composables/useUrl'; + +export const useContributorManagerStore = defineComponentStore( + 'contributorManager', + (props) => { + const {apiUrl: contributorApiUrl} = useUrl( + `submissions/${props.submissionId}/publications/${props.publicationId}/contributors`, + ); + + const {data, fetch: fetchContributors} = useFetch(contributorApiUrl, { + query: {}, + }); + + const contributors = computed(() => data.value?.items || []); + + fetchContributors(); + + return {title: props.title, contributors, fetchContributors}; + }, +); diff --git a/src/managers/FileManager/FileManager.vue b/src/managers/FileManager/FileManager.vue new file mode 100644 index 000000000..41a78b1eb --- /dev/null +++ b/src/managers/FileManager/FileManager.vue @@ -0,0 +1,25 @@ + + diff --git a/src/managers/FileManager/fileManagerStore.js b/src/managers/FileManager/fileManagerStore.js new file mode 100644 index 000000000..18afca9fa --- /dev/null +++ b/src/managers/FileManager/fileManagerStore.js @@ -0,0 +1,29 @@ +import {defineComponentStore} from '@/utils/defineComponentStore'; + +import {ref, computed} from 'vue'; +import {useFetch} from '@/composables/useFetch'; +import {useUrl} from '@/composables/useUrl'; + +export const useFileManagerStore = defineComponentStore( + 'fileManager', + (props) => { + const submissionId = ref(props.submissionId); + + const {apiUrl: filesApiUrl} = useUrl( + `submissions/${submissionId.value}/files`, + ); + + const {data, fetch: fetchFiles} = useFetch(filesApiUrl, { + query: { + fileStages: props.fileStages, + reviewRoundId: props.reviewRoundId, + }, + }); + + const files = computed(() => data.value?.items); + + fetchFiles(); + + return {title: props.title, files, fetchFiles}; + }, +); diff --git a/src/managers/ReviewerManager/ReviewerManager.vue b/src/managers/ReviewerManager/ReviewerManager.vue new file mode 100644 index 000000000..8fa5b6213 --- /dev/null +++ b/src/managers/ReviewerManager/ReviewerManager.vue @@ -0,0 +1,75 @@ + + + diff --git a/src/managers/ReviewerManager/reviewerManagerStore.js b/src/managers/ReviewerManager/reviewerManagerStore.js new file mode 100644 index 000000000..b5f98261e --- /dev/null +++ b/src/managers/ReviewerManager/reviewerManagerStore.js @@ -0,0 +1,24 @@ +import {computed} from 'vue'; +import {defineComponentStore} from '@/utils/defineComponentStore'; +import {useReviewAssignment} from '@/composables/useReviewAssignment'; + +export const useReviewerManagerStore = defineComponentStore( + 'reviewerManagerStore', + (props) => { + const {getReviewMethodIcons, getOpenReviewAssignments} = + useReviewAssignment(); + + const reviewAssignments = computed(() => { + if (props.redactedForAuthors) { + return getOpenReviewAssignments(props.reviewAssignments); + } + + return props.reviewAssignments; + }); + + return { + getReviewMethodIcons, + reviewAssignments, + }; + }, +); diff --git a/src/pages/dashboard/DashboardPage.mdx b/src/pages/dashboard/DashboardPage.mdx new file mode 100644 index 000000000..cf87f50d7 --- /dev/null +++ b/src/pages/dashboard/DashboardPage.mdx @@ -0,0 +1,10 @@ +import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks'; + +import * as DashboardPage from './DashboardPage.stories.js'; + + + +# DashboardPage page + + +./DashboardPage.stories.js diff --git a/src/pages/submissions/SubmissionsPage.stories.js b/src/pages/dashboard/DashboardPage.stories.js similarity index 83% rename from src/pages/submissions/SubmissionsPage.stories.js rename to src/pages/dashboard/DashboardPage.stories.js index 4c1ff7bd8..02edd2c22 100644 --- a/src/pages/submissions/SubmissionsPage.stories.js +++ b/src/pages/dashboard/DashboardPage.stories.js @@ -1,17 +1,17 @@ -import SubmissionsPage from './SubmissionsPage.vue'; +import DashboardPage from './DashboardPage.vue'; import {http, HttpResponse} from 'msw'; import SubmissionsMock25 from './mocks/submissions25.js'; -import PageInitConfigMock from './mocks/pageInitConfig'; +import PageInitConfigMock from './mocks/pageInitConfig.js'; -export default {title: 'Pages/Submissions', component: SubmissionsPage}; +export default {title: 'Pages/Dashboard', component: DashboardPage}; export const Init = { render: (args) => ({ - components: {SubmissionsPage}, + components: {DashboardPage}, setup() { return {args}; }, - template: '', + template: '', }), parameters: { // mock date to consistently show sensible editorial activity popups diff --git a/src/pages/submissions/SubmissionsPage.vue b/src/pages/dashboard/DashboardPage.vue similarity index 76% rename from src/pages/submissions/SubmissionsPage.vue rename to src/pages/dashboard/DashboardPage.vue index 3efa0d6a5..626d3181b 100644 --- a/src/pages/submissions/SubmissionsPage.vue +++ b/src/pages/dashboard/DashboardPage.vue @@ -1,5 +1,5 @@