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 17, 2024
1 parent 7cd7278 commit 9e4e41a
Show file tree
Hide file tree
Showing 27 changed files with 1,017 additions and 233 deletions.
15 changes: 13 additions & 2 deletions public/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,8 @@ window.pkp = {
'submissions.declined': 'Declined',
'submissions.incomplete': 'Incomplete',
todo: '##todo##',
'user.email': 'Email',
'user.emailAddress': 'Email Address',
'about.contact.email': 'Email',
'user.email': 'Email Address',
'user.orcid': 'ORCID iD',
'user.username': 'Username',
'user.password': 'Password',
Expand Down Expand Up @@ -560,6 +560,17 @@ window.pkp = {
'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':'Dose not appear on the masthead',
},

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/TableNext/Table.vue';
import TableCell from '@/components/TableNext/TableCell.vue';
import TableHeader from '@/components/TableNext/TableHeader.vue';
import TableColumn from '@/components/TableNext/TableColumn.vue';
import TableBody from '@/components/TableNext/TableBody.vue';
import TableRow from '@/components/TableNext/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>
141 changes: 141 additions & 0 deletions src/managers/InvitationManager/InvitationManagerStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 twith 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 givenName + ' ' + familyName;
}

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,
// invitation table data
invitations,
isInvitationLoading,
getFullName,
handleReviewAction,
};
},
);
Loading

0 comments on commit 9e4e41a

Please sign in to comment.