Skip to content

Commit

Permalink
invitation table view and api integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ipula committed Sep 20, 2024
1 parent f77229c commit 3e77ca5
Show file tree
Hide file tree
Showing 27 changed files with 1,193 additions and 276 deletions.
43 changes: 43 additions & 0 deletions public/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,13 @@ window.pkp = {
'submissions.declined': 'Declined',
'submissions.incomplete': 'Incomplete',
todo: '##todo##',
'about.contact.email': 'Email',
'user.email': 'Email Address',
'user.orcid': 'ORCID iD',
'user.username': 'Username',
'user.password': 'Password',
'user.givenName': 'Given Name',
'user.familyName': 'Family Name',
'user.affiliation': 'Affiliation',
'user.email': 'Email',
'user.emailAddress': '##user.emailAddress##',
Expand All @@ -720,6 +727,42 @@ window.pkp = {
'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. [email protected]',
'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: <ul><li>Email Address : {$email}</li><li>Role : {$roles}</li><li>Status : {$status}</li><li>Affiliation : {$affiliation}</li></ul>',
'invitation.role.modifyRole.button':'Modify Role',
'invitation.masthead.show':'Appear on the masthead',
'invitation.masthead.hidden':'Does not appear on the masthead',
'invitation.roles':'Roles',
'invitation.removeRoles':'Remove Role',
},

tinyMCE: {
Expand Down
9 changes: 9 additions & 0 deletions src/managers/InvitationManager/InvitationManager.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';

import * as InvitationManager from './InvitationManager.stories.js';

<Meta of={InvitationManager} />

# Invitation page

<ArgTypes />
50 changes: 50 additions & 0 deletions src/managers/InvitationManager/InvitationManager.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import InvitationManager from './InvitationManager.vue';
import {http, HttpResponse} from 'msw';
import invitationMock from './mocks/invitationMock.js';

export default {
title: 'Managers/InvitationManager',
component: InvitationManager,
};

export const Init = {
render: (args) => ({
components: {InvitationManager},
setup() {
return {args};
},
template: '<InvitationManager v-bind="args"/>',
}),
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: [],
};
107 changes: 107 additions & 0 deletions src/managers/InvitationManager/InvitationManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<template>
<div
v-if="!isInvitationLoading"
class="flex items-center pb-2 pt-2 text-3xl-normal"
>
<h4>
{{ t('invitation.header') }}({{ store.invitationsPagination.itemCount }})
</h4>
<div class="ml-auto">
<PkpButton @click="store.sendInvitation">
{{ t('invitation.inviteToRole.btn') }}
</PkpButton>
</div>
</div>
<PkpTable aria-label="Example for basic table">
<TableHeader>
<TableColumn>{{ t('invitation.tableHeader.name') }}</TableColumn>
<TableColumn>{{ t('about.contact.email') }}</TableColumn>
<TableColumn>{{ t('invitation.header') }}</TableColumn>
<TableColumn>{{ t('common.status') }}</TableColumn>
<TableColumn>{{ t('user.affiliation') }}</TableColumn>
<TableColumn></TableColumn>
</TableHeader>
<TableBody>
<TableRow v-for="(row, index) in store.invitations" :key="index">
<TableCell>
{{
row.userId
? row.existingUser.fullName
: store.getFullName(row.givenName, row.familyName)
}}
<Icon icon="orcid" :inline="true" />
</TableCell>
<TableCell>
{{ row.userId ? row.existingUser.email : row.email }}
</TableCell>
<TableCell>
<template v-for="(userGroups, i) in row.userGroupsToAdd" :key="i">
<span>
{{ localize(userGroups.userGroupName) }}
</span>
<br />
</template>
</TableCell>
<TableCell>
Invited
{{ new Date(row.createdAt).toLocaleDateString('en-US') }}
</TableCell>
<TableCell>
{{
row.userId
? localize(row.existingUser.affiliation)
: localize(row.affiliation)
}}
</TableCell>
<TableCell>
<DropdownActions
:actions="[
{
label: t('common.edit'),
url: store.pageUrl + '/' + row.id,
icon: 'Edit',
},
{
label: t('invitation.cancelInvite.actionName'),
icon: 'Cancel',
name: 'cancelInvite',
isWarnable: true,
},
]"
label="Invitation management options"
:display-as-ellipsis="true"
direction="left"
@action="(actionName) => store.handleReviewAction(row)"
/>
</TableCell>
</TableRow>
</TableBody>
</PkpTable>
<div class="flex justify-end">
<Pagination
:current-page="store.invitationsPagination.currentPage"
:last-page="store.invitationsPagination.pageCount"
:is-loading="store.isInvitationLoading"
:show-adjacent-pages="3"
@set-page="store.setCurrentPage"
/>
</div>
</template>

<script setup>
import PkpTable from '@/components/Table/Table.vue';
import TableCell from '@/components/Table/TableCell.vue';
import TableHeader from '@/components/Table/TableHeader.vue';
import TableColumn from '@/components/Table/TableColumn.vue';
import TableBody from '@/components/Table/TableBody.vue';
import TableRow from '@/components/Table/TableRow.vue';
import Icon from '@/components/Icon/Icon.vue';
import PkpButton from '@/components/Button/Button.vue';
import {useInvitationManagerStore} from './InvitationManagerStore.js';
import Pagination from '@/components/Pagination/Pagination.vue';
import {useTranslation} from '@/composables/useTranslation';
import DropdownActions from '@/components/DropdownActions/DropdownActions.vue';
const store = useInvitationManagerStore();
const {t} = useTranslation();
</script>
146 changes: 146 additions & 0 deletions src/managers/InvitationManager/InvitationManagerStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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 {computed, ref, watch} from 'vue';

export const useInvitationManagerStore = defineComponentStore(
'invitationsPage',
() => {
const {openDialog} = useModal();
const {localize} = useLocalize();
const {t} = useTranslation();
/** Announcer */

const {announce} = useAnnouncer();
/**
* redirect to send invitation page
*/
const {pageUrl} = useUrl('invitation/invite');
const sendInvitationPageUrl = computed(() => {
return pageUrl.value;
});
function sendInvitation() {
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 getInvitationApiUrl = computed(() => {
return apiUrl.value;
});
const {
items: invitations,
pagination: invitationsPagination,
isLoading: isInvitationLoading,
fetch: fetchInvitation,
} = useFetchPaginated(getInvitationApiUrl, {
currentPage,
pageSize: countPerPage,
});
watch(
[currentPage],
async () => {
announce(t('common.loading'));

await fetchInvitation();
announce(t('common.loaded'));
},
{immediate: true},
);

/**
* create user full name
* @param String givenName
* @param String familyName
* @returns String
*/
function getFullName(givenName, familyName) {
return localize(givenName) + ' ' + localize(familyName);
}

/**
* get all invited user groups
* @param {Array} userGroups
* @returns
*/
function getAllInvitedRoles(userGroups) {
let roles = '';
userGroups.forEach((element) => {
roles = roles + localize(element.userGroupName) + ', ';
});

return roles;
}

function handleReviewAction(data) {
openDialog({
title: t('invitation.cancelInvite.title'),
message: t('invitation.cancelInvite.message', {
givenName: data.givenName,
familyName: data.familyName,
email: data.email,
roles: getAllInvitedRoles(data.userGroupsToAdd),
status:
'invited ' + new Date(data.createdAt).toLocaleDateString('en-US'),
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 fetchInvitation();
announce(t('common.loaded'));
close();
},
},
{
label: t('common.cancel'),
isWarnable: true,
callback: (close) => {},
},
],
});
}

return {
invitationCount,
setCurrentPage,
sendInvitation,
currentPage,
invitationsPagination,
pageUrl,

invitations,
isInvitationLoading,
getFullName,
handleReviewAction,
};
},
);
Loading

0 comments on commit 3e77ca5

Please sign in to comment.