From 0b8df108d689db4a2e325b7eb008da32bc04b572 Mon Sep 17 00:00:00 2001 From: ipula Date: Wed, 2 Oct 2024 13:11:10 +0200 Subject: [PATCH] invitation table view and api integration --- public/globals.js | 92 +++++---- .../UserInvitationManager.mdx | 11 ++ .../UserInvitationManager.stories.js | 50 +++++ .../UserInvitationManager.vue | 116 +++++++++++ ...ationManagerCancelInvitationDialogBody.vue | 27 +++ .../UserInvitationManagerStore.js | 176 +++++++++++++++++ .../mocks/invitationMock.js | 107 +++++++++++ .../AcceptInvitationPage.stories.js | 16 +- .../acceptInvitation/AcceptInvitationPage.vue | 5 +- .../AcceptInvitationPageStore.js | 84 ++++++-- .../AcceptInvitationReview.vue | 104 +++++++--- .../AcceptInvitationUserAccountDetails.vue | 11 +- .../AcceptInvitationUserDetailsForms.vue | 49 ++++- .../AcceptInvitationUserRoles.vue | 34 +++- .../AcceptInvitationVerifyOrcid.vue | 1 + .../mocks/invitationNewUserMock.js | 63 ++++-- .../invitationUserNotVerifiedOrcidMock.js | 69 +++++-- .../mocks/invitationUserVerifiedOrcidMock.js | 69 +++++-- .../acceptInvitation/mocks/pageInitConfig.js | 31 +-- .../pageInitConfigUserNotVerifiedOrcid.js | 8 +- .../UserInvitationDetailsFormStep.vue | 72 +++++-- .../UserInvitationEmailComposerStep.vue | 21 +- .../UserInvitationPage.stories.js | 53 +++--- .../userInvitation/UserInvitationPage.vue | 22 ++- .../userInvitation/UserInvitationPageStore.js | 180 +++++++++++++----- .../UserInvitationSearchFormStep.vue | 51 +++-- .../UserInvitationUserGroupsTable.vue | 80 ++++++-- .../userInvitation/mocks/pageInitConfig.js | 46 ++--- 28 files changed, 1318 insertions(+), 330 deletions(-) create mode 100644 src/managers/UserInvitationManager/UserInvitationManager.mdx create mode 100644 src/managers/UserInvitationManager/UserInvitationManager.stories.js create mode 100755 src/managers/UserInvitationManager/UserInvitationManager.vue create mode 100644 src/managers/UserInvitationManager/UserInvitationManagerCancelInvitationDialogBody.vue create mode 100755 src/managers/UserInvitationManager/UserInvitationManagerStore.js create mode 100644 src/managers/UserInvitationManager/mocks/invitationMock.js diff --git a/public/globals.js b/public/globals.js index 52d3e0337..cea2baf1e 100644 --- a/public/globals.js +++ b/public/globals.js @@ -119,23 +119,6 @@ window.pkp = { * Locale keys loaded on the server-side */ localeKeys: { - 'acceptInvitation.modal.button': '##acceptInvitation.modal.button##', - 'acceptInvitation.modal.message': '##acceptInvitation.modal.message##', - 'acceptInvitation.modal.title': '##acceptInvitation.modal.title##', - 'acceptInvitation.passwordField.description': - '##acceptInvitation.passwordField.description##', - 'acceptInvitation.privacyStatement.btn': - '##acceptInvitation.privacyStatement.btn##', - 'acceptInvitation.privacyStatement.label': - '##acceptInvitation.privacyStatement.label##', - 'acceptInvitation.review.accountDetails': - '##acceptInvitation.review.accountDetails##', - 'acceptInvitation.review.userDetails': - '##acceptInvitation.review.userDetails##', - 'acceptInvitation.skipVerifyOrcid': '##acceptInvitation.skipVerifyOrcid##', - 'acceptInvitation.usernameField.description': - '##acceptInvitation.usernameField.description##', - 'acceptInvitation.verifyOrcid': '##acceptInvitation.verifyOrcid##', 'admin.jobs.failed.action.redispatch': 'Try Again', 'admin.jobs.failed.action.redispatch.all': 'Requeue All Failed Jobs', 'article.article': 'Article', @@ -441,14 +424,12 @@ window.pkp = { 'Log in as this user? All actions you perform will be attributed to this user.', 'help.help': 'Help', 'informationCenter.informationCenter': 'Information Center', - 'invitation.orcid.acceptInvitation.message': - '##invitation.orcid.acceptInvitation.message##', - 'invitation.role.addRole.button': '##invitation.role.addRole.button##', - 'invitation.role.dateStart': '##invitation.role.dateStart##', - 'invitation.role.masthead': '##invitation.role.masthead##', + 'invitation.role.addRole.button': 'Add Another Role', + 'invitation.role.dateStart': 'Start Date', + 'invitation.role.masthead': 'Journal Masthead', 'invitation.role.removeRole.button': - '##invitation.role.removeRole.button##', - 'invitation.role.selectRole': '##invitation.role.selectRole##', + 'Remove Role', + 'invitation.role.selectRole': 'Select a new role', 'invitation.wizard.completeSteps': '##invitation.wizard.completeSteps##', 'invitation.wizard.errors': '##invitation.wizard.errors##', 'issue.issue': 'Issue', @@ -695,31 +676,60 @@ window.pkp = { 'submissions.declined': 'Declined', 'submissions.incomplete': 'Incomplete', todo: '##todo##', + 'about.contact.email': 'Email', + 'user.email': 'Email Address', 'user.affiliation': 'Affiliation', - 'user.email': 'Email', - 'user.emailAddress': '##user.emailAddress##', 'user.familyName': 'Family Name', 'user.givenName': 'Given Name', 'user.gossip': 'Editorial Notes', 'user.orcid': 'ORCID iD', 'user.password': 'Password', 'user.username': 'Username', - 'userInvitation.emailField.description': - '##userInvitation.emailField.description##', - 'userInvitation.modal.button': '##userInvitation.modal.button##', - 'userInvitation.modal.message': '##userInvitation.modal.message##', - 'userInvitation.modal.title': '##userInvitation.modal.title##', - 'userInvitation.orcidField.description': - '##userInvitation.orcidField.description##', - 'userInvitation.roleTable.endDate': '##userInvitation.roleTable.endDate##', - 'userInvitation.roleTable.journalMasthead': - '##userInvitation.roleTable.journalMasthead##', - 'userInvitation.roleTable.role': '##userInvitation.roleTable.role##', - 'userInvitation.roleTable.startDate': - '##userInvitation.roleTable.startDate##', - 'userInvitation.usernameField.description': - '##userInvitation.usernameField.description##', 'validator.required': 'This field is required.', + 'invitation.notification.closeBtn':'View all users', + 'invitation.orcid.acceptInvitation.message':'Not verified. You can verify your ORCID iD from your profile section in OJS', + 'userInvitation.emailField.description':'e.g. aeinstein@example.com', + 'userInvitation.usernameField.description':'e.g. mickeymouse', + 'userInvitation.orcidField.description':'e.g. 0000-0000-0000-0000', + 'userInvitation.roleTable.role':'Role', + 'userInvitation.roleTable.startDate':'Start Date', + 'userInvitation.roleTable.endDate':'End Date', + 'userInvitation.roleTable.journalMasthead':'Journal Masthead', + 'userInvitation.modal.title':'Invitation Sent', + 'userInvitation.modal.message':'{$email} has been invited to new role in OJS.You can be updated about users on the User and Roles page, your ojs notification and/ or your email', + 'userInvitation.modal.button':'View All Users', + 'acceptInvitation.usernameField.description':'It should be 10 characters long and could be a combination of uppercase letters, lowercase letters or numbers', + 'acceptInvitation.passwordField.description':'It should be 12 characters long and should be a combination of uppercase letters, lowercase letters, numbers and symbols', + 'acceptInvitation.review.userDetails':'User Details', + 'acceptInvitation.review.accountDetails':'Account Details', + 'acceptInvitation.verifyOrcid':'Verify ORCID iD', + 'acceptInvitation.skipVerifyOrcid':'Skip ORCID verification', + 'acceptInvitation.modal.title':"You've been assigned a new role in OJS", + 'acceptInvitation.modal.message':'Congratulations on your new role in OJS! You might now have access to new options. If you need assistance navigating the system, please click on the “Help” buttons throughout the interface for guidance', + 'acceptInvitation.modal.button':'View All Submissions', + 'acceptInvitation.privacyStatement.btn':'Privacy Statement', + 'acceptInvitation.privacyStatement.label':'Yes, I agree to have my data collected and stored according to the', + 'invitation.cancel': 'Cancel Invite', + 'invitation.inviteToRole.btn': 'Invite to a role', + 'invitation.header': 'Invitation', + 'invitation.tableHeader.name': 'Name', + 'invitation.searchForm.emptyError': 'At least provide one search criteria.', + 'invitation.cancelInvite.actionName':'Cancel Invite', + 'invitation.cancelInvite.title':'Cancel Invitation', + 'invitation.cancelInvite.message': 'Cancel the invitation sent to {$givenName} {$familyName} will deactivate acceptance link sent via email. Here are the invitation details:', + 'invitation.role.modifyRole.button':'Modify Role', + 'invitation.masthead.show':'Appear on the masthead', + 'invitation.masthead.hidden':'Does not appear on the masthead', + 'invitation.removeRoles':'Remove Role', + 'invitation.management.options':'Invitation management options', + 'userInvitation.cancel.message':'Are you sure wnat to cancel this invitation ?', + 'userInvitation.cancel.keepWorking':'Keep Working', + 'userInvitation.status.invited':'Invited', + 'userInvitation.search.userNotFound':'The user does not have a role in this journal', + 'userInvitation.search.userFound':'The user already exists in the journal', + 'userInvitation.edit.title':'Edit Invitation', + 'userInvitation.edit.message':'If you edit the existing invitation or add a new role, the current invitation will be canceled and, a new one will be sent. Are you sure you want to proceed?', + 'invitation.step':'STEP', }, tinyMCE: { diff --git a/src/managers/UserInvitationManager/UserInvitationManager.mdx b/src/managers/UserInvitationManager/UserInvitationManager.mdx new file mode 100644 index 000000000..fdb382dfd --- /dev/null +++ b/src/managers/UserInvitationManager/UserInvitationManager.mdx @@ -0,0 +1,11 @@ +import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks'; + +import * as UserInvitationManager from './UserInvitationManager.stories.js'; + + + +# User Invitation page + +This page will show the current pending invitation list and handles the edit and cancel invite. + + diff --git a/src/managers/UserInvitationManager/UserInvitationManager.stories.js b/src/managers/UserInvitationManager/UserInvitationManager.stories.js new file mode 100644 index 000000000..bb45f0b88 --- /dev/null +++ b/src/managers/UserInvitationManager/UserInvitationManager.stories.js @@ -0,0 +1,50 @@ +import UserInvitationManager from './UserInvitationManager.vue'; +import {http, HttpResponse} from 'msw'; +import invitationMock from './mocks/invitationMock.js'; + +export default { + title: 'Managers/UserInvitationManager', + component: UserInvitationManager, +}; + +export const Init = { + render: (args) => ({ + components: {UserInvitationManager}, + setup() { + return {args}; + }, + template: '', + }), + parameters: { + msw: { + handlers: [ + http.get( + 'https://mock/index.php/publicknowledge/api/v1/invitations', + async ({request}) => { + const url = new URL(request.url); + const offset = parseInt(url.searchParams.get('offset') || 0); + const count = parseInt(url.searchParams.get('count')); + const invitations = invitationMock.items.slice( + offset, + offset + count, + ); + + return HttpResponse.json({ + itemsMax: invitationMock.itemsMax, + items: invitations, + }); + }, + ), + http.post( + 'https://mock/index.php/publicknowledge/api/v1/invitations/1/cancel', + async ({request}) => { + return HttpResponse.json({ + request, + }); + }, + ), + ], + }, + }, + args: [], +}; diff --git a/src/managers/UserInvitationManager/UserInvitationManager.vue b/src/managers/UserInvitationManager/UserInvitationManager.vue new file mode 100755 index 000000000..dd43fcb7a --- /dev/null +++ b/src/managers/UserInvitationManager/UserInvitationManager.vue @@ -0,0 +1,116 @@ + + + diff --git a/src/managers/UserInvitationManager/UserInvitationManagerCancelInvitationDialogBody.vue b/src/managers/UserInvitationManager/UserInvitationManagerCancelInvitationDialogBody.vue new file mode 100644 index 000000000..762859e7b --- /dev/null +++ b/src/managers/UserInvitationManager/UserInvitationManagerCancelInvitationDialogBody.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/managers/UserInvitationManager/UserInvitationManagerStore.js b/src/managers/UserInvitationManager/UserInvitationManagerStore.js new file mode 100755 index 000000000..5816e5633 --- /dev/null +++ b/src/managers/UserInvitationManager/UserInvitationManagerStore.js @@ -0,0 +1,176 @@ +import {defineComponentStore} from '@/utils/defineComponentStore'; +import {useApiUrl} from '@/composables/useApiUrl'; +import {useAnnouncer} from '@/composables/useAnnouncer'; +import {useUrl} from '@/composables/useUrl'; +import {useLocalize} from '@/composables/useLocalize'; +import {useTranslation} from '@/composables/useTranslation'; +import {useFetchPaginated} from '@/composables/useFetchPaginated'; +import {useFetch} from '@/composables/useFetch'; +import {useModal} from '@/composables/useModal'; +import {useDate} from '@/composables/useDate'; +import {ref, watch} from 'vue'; +import InvitationManagerCancelInvitationDialogBody from './UserInvitationManagerCancelInvitationDialogBody.vue'; + +export const useUserInvitationManagerStore = defineComponentStore( + 'userInvitationsPage', + () => { + const {openDialog} = useModal(); + const {localize} = useLocalize(); + const {t} = useTranslation(); + const {formatShortDate} = useDate(); + /** Announcer */ + + const {announce} = useAnnouncer(); + /** + * redirect to send invitation page + */ + const {pageUrl: sendInvitationPageUrl} = useUrl('invitation/invite'); + function createNewInvitation() { + window.location = sendInvitationPageUrl.value; + } + + /** + * get invitations with paginations + */ + const invitationCount = ref(0); + + const countPerPage = ref(2); + const currentPage = ref(1); + function setCurrentPage(_currentPage) { + currentPage.value = _currentPage; + } + + const {apiUrl} = useApiUrl('invitations'); + const { + items: invitations, + pagination: invitationsPagination, + isLoading: isInvitationLoading, + fetch: fetchInvitations, + } = useFetchPaginated(apiUrl, { + currentPage, + pageSize: countPerPage, + }); + watch( + [currentPage], + async () => { + announce(t('common.loading')); + + await fetchInvitations(); + announce(t('common.loaded')); + }, + {immediate: true}, + ); + + /** + * get all invited user groups + * @param {Array} userGroups + * @returns + */ + function getAllInvitedRoles(userGroups) { + let roles = ''; + userGroups.forEach((element) => { + roles = + roles + + localize(element.userGroupName) + + t('common.commaListSeparator'); + }); + + return roles; + } + + function handleInvitationAction(actionName, data) { + console.log(actionName, data); + if (actionName === 'editInvite') { + handleEditInvitation(data); + } else { + handleCancelInvitation(data); + } + } + + function handleEditInvitation(data) { + openDialog({ + title: t('userInvitation.edit.title'), + message: t('userInvitation.edit.message'), + actions: [ + { + label: t('userInvitation.edit.title'), + isPrimary: true, + callback: async (close) => { + window.location = sendInvitationPageUrl.value + '/' + data.id; + }, + }, + { + label: t('common.cancel'), + isWarnable: true, + callback: (close) => { + close(); + }, + }, + ], + }); + } + + function handleCancelInvitation(data) { + openDialog({ + title: t('invitation.cancelInvite.title'), + bodyComponent: InvitationManagerCancelInvitationDialogBody, + bodyProps: { + message: t('invitation.cancelInvite.message', { + givenName: data.existingUser + ? data.existingUser.givenName + : data.givenName, + familyName: data.existingUser + ? data.existingUser.familyName + : data.familyName, + }), + email: data.existingUser ? data.existingUser.email : data.email, + roles: getAllInvitedRoles(data.userGroupsToAdd), + status: + t('userInvitation.status.invited') + + formatShortDate(data.createdAt), + affiliation: data.affiliation ? data.affiliation : '', + }, + actions: [ + { + label: t('invitation.cancelInvite.title'), + isPrimary: true, + callback: async (close) => { + const {apiUrl: cancelApiUrl} = useUrl( + `invitations/${data.id}/cancel`, + ); + const {fetch: cancelInvitation} = useFetch(cancelApiUrl.value, { + method: 'PUT', + body: {}, + }); + + announce(t('common.loading')); + await cancelInvitation(); + await fetchInvitations(); + announce(t('common.loaded')); + close(); + }, + }, + { + label: t('common.cancel'), + isWarnable: true, + callback: (close) => { + close(); + }, + }, + ], + }); + } + + return { + invitationCount, + setCurrentPage, + createNewInvitation, + currentPage, + invitationsPagination, + + invitations, + isInvitationLoading, + handleInvitationAction, + }; + }, +); diff --git a/src/managers/UserInvitationManager/mocks/invitationMock.js b/src/managers/UserInvitationManager/mocks/invitationMock.js new file mode 100644 index 000000000..51a189bb3 --- /dev/null +++ b/src/managers/UserInvitationManager/mocks/invitationMock.js @@ -0,0 +1,107 @@ +export default { + itemsMax: 2, + items: [ + { + id: 1, + status: 'PENDING', + createdAt: '2024-09-10T20:31:10.000000Z', + updatedAt: '2024-09-10T20:31:15.000000Z', + userId: null, + contextId: 1, + expiryDate: '2024-09-13T20:31:13.000000Z', + email: 'test@mailinator.com', + inviterId: 1, + orcid: null, + givenName: {en: 'Test', fr_CA: 'Test_fr'}, + familyName: {en: 'Test', fr_CA: 'Test_fr'}, + affiliation: null, + country: null, + emailSubject: null, + emailBody: null, + userGroupsToAdd: [ + { + userGroupId: 2, + userGroupName: { + en: 'Journal manager', + fr_CA: 'Directeur-trice de la revue', + }, + masthead: true, + dateStart: '2024-09-10', + dateEnd: null, + }, + ], + userGroupsToRemove: [], + username: null, + sendEmailAddress: 'journalmanager@mailinator.com', + newUser: { + email: 'test@mailinator.com', + fullName: 'Test Test', + familyName: null, + givenName: null, + country: null, + affiliation: null, + orcid: null, + }, + existingUser: null, + }, + { + id: 2, + status: 'PENDING', + createdAt: '2024-09-10T20:24:53.000000Z', + updatedAt: '2024-09-10T20:25:04.000000Z', + userId: 35, + contextId: 1, + expiryDate: '2024-09-13T20:25:02.000000Z', + email: null, + inviterId: 1, + orcid: null, + givenName: null, + familyName: null, + affiliation: null, + country: null, + emailSubject: null, + emailBody: null, + userGroupsToAdd: [ + { + userGroupId: 16, + userGroupName: { + en: 'Reviewer', + fr_CA: '\u00c9valuateur-trice', + }, + masthead: true, + dateStart: '2024-09-10', + dateEnd: null, + }, + ], + userGroupsToRemove: [ + { + userGroupId: 9, + userGroupName: { + en: 'Funding coordinator', + fr_CA: 'Coordonnateur-trice du financement', + }, + masthead: null, + dateStart: null, + dateEnd: null, + }, + ], + username: null, + sendEmailAddress: 'zwoods@mailinator.com', + newUser: null, + existingUser: { + email: 'zwoods@mailinator.com', + fullName: 'Zita Woods', + familyName: { + en: 'Woods', + }, + givenName: { + en: 'Zita', + }, + country: 'US', + affiliation: { + en: 'CUNY', + }, + }, + }, + ], +}; diff --git a/src/pages/acceptInvitation/AcceptInvitationPage.stories.js b/src/pages/acceptInvitation/AcceptInvitationPage.stories.js index b76cb54bb..6bc5101b1 100644 --- a/src/pages/acceptInvitation/AcceptInvitationPage.stories.js +++ b/src/pages/acceptInvitation/AcceptInvitationPage.stories.js @@ -35,31 +35,31 @@ export const NewUser = { const postBody = await request.json(); let errors = {}; - if (!Object.keys(postBody).includes('username')) { + if (!Object.keys(postBody.invitationData).includes('username')) { errors['username'] = ['This field is required']; } - if (!Object.keys(postBody).includes('password')) { + if (!Object.keys(postBody.invitationData).includes('password')) { errors['password'] = ['This field is required']; } - if (!Object.keys(postBody).includes('affiliation')) { + if (!Object.keys(postBody.invitationData).includes('affiliation')) { errors['affiliation'] = ['This field is required']; } - if (!Object.keys(postBody).includes('familyName')) { + if (!Object.keys(postBody.invitationData).includes('familyName')) { errors['familyName'] = ['This field is required']; } - if (!Object.keys(postBody).includes('country')) { + if (!Object.keys(postBody.invitationData).includes('userCountry')) { errors['country'] = ['This field is required']; } - Object.keys(postBody).forEach((element) => { + Object.keys(postBody.invitationData).forEach((element) => { if (element !== 'orcid') { - if (postBody[element] === '') { + if (postBody.invitationData[element] === '') { errors[element] = ['This field is required']; } } }); if (Object.keys(errors).length > 0) { - return HttpResponse.json(errors, {status: 422}); + return HttpResponse.json({errors: errors}, {status: 422}); } return HttpResponse.json(postBody, {status: 200}); }, diff --git a/src/pages/acceptInvitation/AcceptInvitationPage.vue b/src/pages/acceptInvitation/AcceptInvitationPage.vue index af7d15907..1015fa3d2 100644 --- a/src/pages/acceptInvitation/AcceptInvitationPage.vue +++ b/src/pages/acceptInvitation/AcceptInvitationPage.vue @@ -1,7 +1,7 @@