Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkp/pkp-lib#9658 user access table and table actions #437

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions public/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ window.pkp = {
'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.email': 'Email',
'email.subject': 'Subject',
'email.to': 'To',
'fileManager.copyeditedFiles': 'Copyedited Files',
Expand Down Expand Up @@ -439,18 +440,22 @@ window.pkp = {
'grid.action.deleteContributor': 'Delete Contributor',
'grid.action.deleteContributor.confirmationMessage':
'Are you sure you want to remove {$name} as a contributor? This action can not be undone.',
'grid.action.disable': 'Disable User',
'grid.action.edit': 'Edit',
'grid.action.editFile': 'Edit a file',
'grid.action.logInAs': 'Login As',
'grid.action.moreInformation': 'More Information',
'grid.action.order': 'Order',
'grid.action.remove': 'Remove',
'grid.action.saveOrdering': 'Save Order',
'grid.action.sort': 'Sort',
'grid.columns.actions': 'Actions',
'grid.libraryFiles.submission.title': 'Submission Library',
'grid.noItems': 'No Items',
'grid.user.confirmLogInAs':
'Log in as this user? All actions you perform will be attributed to this user.',
'grid.user.currentUsers':'Current Users',
'grid.action.mergeUser':'Merge User',
'help.help': 'Help',
'informationCenter.informationCenter': 'Information Center',
'invitation.cancelInvite.actionName': 'Cancel Invite',
Expand Down Expand Up @@ -770,6 +775,8 @@ window.pkp = {
'Are you sure want remove this role permanently?',
'user.role.reviewer': 'Reviewer',
'user.role.reviewers': 'Reviewers',
'user.roles': 'Roles',
'user.startDate': 'Start Date',
'user.username': 'Username',
'userInvitation.cancel.goBack': 'Go Back',
'userInvitation.cancel.message':
Expand Down
11 changes: 11 additions & 0 deletions src/managers/UserAccessManager/UserAccessManager.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';

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

<Meta of={UserAccessManager} />

# User Access Manager

This table displays the current list of users ns and allows the user access manager to search users.

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

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

export const Init = {
render: (args) => ({
components: {UserAccessManager},
setup() {
return {args};
},
template: '<UserAccessManager v-bind="args"/>',
}),
parameters: {
msw: {
handlers: [
http.get(
'https://mock/index.php/publicknowledge/api/v1/users',
async ({request}) => {
const url = new URL(request.url);
const offset = parseInt(url.searchParams.get('offset') || 0);
const count = parseInt(url.searchParams.get('count'));
const users = userAccessMock.items.slice(offset, offset + count);

return HttpResponse.json({
itemsMax: userAccessMock.itemsMax,
items: users,
});
},
),
],
},
},
args: [],
};
88 changes: 88 additions & 0 deletions src/managers/UserAccessManager/UserAccessManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<PkpTable class="mt-2">
<template #label>
<h3 class="text-3xl-bold">
{{ t('grid.user.currentUsers') }}({{
store.userAccessPagination.itemCount
}})
</h3>
</template>
<template #top-controls>
<Search
:search-phrase="searchPhrase"
:search-label="t('userAccess.search')"
@search-phrase-changed="store.setSearchPhrase"
/>
</template>
<TableHeader>
<TableColumn v-for="(column, i) in store.columns" :key="i">
<span :class="column.headerSrOnly ? 'sr-only' : ''">
{{ column.header }}
</span>
</TableColumn>
</TableHeader>
<TableBody>
<TableRow v-for="(user, index) in store.userList" :key="index">
<TableCell>
{{ user.fullName }}
<Icon v-if="user.orcidIsVerified" icon="orcid" :inline="true" />
</TableCell>
<TableCell>
{{ user.email }}
</TableCell>
<TableCell>
<template v-for="(userGroups, i) in user.groups" :key="i">
<div class="flex flex-col">
{{ userGroups.name }}
</div>
</template>
</TableCell>
<TableCell>
<template v-for="(userGroups, i) in user.groups" :key="i">
<div class="flex flex-col">
{{ formatShortDate(userGroups?.startDate) }}
</div>
</template>
</TableCell>
<TableCell>
{{ localize(user.affiliation) }}
</TableCell>
<TableCell>
<DropdownActions
:actions="store.getItemActions(user)"
:label="t('userAccess.management.options')"
:display-as-ellipsis="true"
direction="left"
@action="(actionName) => store[actionName]({user})"
/>
</TableCell>
</TableRow>
</TableBody>
</PkpTable>
<TablePagination
:pagination="store.userAccessPagination"
@set-page="store.setCurrentPage"
/>
</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 {useUserAccessManagerStore} from './UserAccessManagerStore.js';
import TablePagination from '@/components/Table/TablePagination.vue';
import {useTranslation} from '@/composables/useTranslation';
import {useDate} from '@/composables/useDate';
import DropdownActions from '@/components/DropdownActions/DropdownActions.vue';
import Search from '@/components/Search/Search.vue';
import {ref} from 'vue';

const store = useUserAccessManagerStore();
const {t} = useTranslation();
const {formatShortDate} = useDate();
const searchPhrase = ref('');
</script>
151 changes: 151 additions & 0 deletions src/managers/UserAccessManager/UserAccessManagerStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {defineComponentStore} from '@/utils/defineComponentStore';
import {useApiUrl} from '@/composables/useApiUrl';
import {useUrl} from '@/composables/useUrl';
import {useAnnouncer} from '@/composables/useAnnouncer';
import {useTranslation} from '@/composables/useTranslation';
import {useFetchPaginated} from '@/composables/useFetchPaginated';
import {ref, watch} from 'vue';
import {useUserAccessManagerActions} from './useUserAccessManagerActions';

export const useUserAccessManagerStore = defineComponentStore(
'userAccessManager',
() => {
const {t} = useTranslation();
/** Announcer */

const {announce} = useAnnouncer();

/**
* Get users with paginations
*/
const userAccessCount = ref(0);

const countPerPage = ref(25);
const currentPage = ref(1);
async function setCurrentPage(_currentPage) {
currentPage.value = _currentPage;
}

const searchPhrase = ref('');
async function setSearchPhrase(val) {
searchPhrase.value = val;
}

const {apiUrl} = useApiUrl('users');
const {
items: userList,
pagination: userAccessPagination,
isLoading: isUserAccessLoading,
fetch: fetchUserList,
} = useFetchPaginated(apiUrl, {
currentPage,
pageSize: countPerPage,
query: {
searchPhrase: searchPhrase,
status: 'all',
// includePermissions: true,
},
});
watch(
[currentPage, searchPhrase],
async () => {
announce(t('common.loading'));
await fetchUserList();
announce(t('common.loaded'));
},
{immediate: true},
);

async function triggerDataChangeCallback() {
await fetchUserList();
}

/**
* User access table columns
* @returns array
*/
function getColumns() {
const columns = [];

columns.push({
header: t('userAccess.tableHeader.name'),
});

columns.push({
header: t('about.contact.email'),
});

columns.push({
header: t('user.roles'),
});
columns.push({
header: t('userAccess.tableHeader.startDate'),
});
columns.push({
header: t('user.affiliation'),
});

columns.push({
header: t('common.moreActions'),
headerSrOnly: true,
});

return columns;
}
/**
* Actions
*/

/*
* redirect to send invitation page
*/
function editUser({user}) {
const {redirectToPage} = useUrl(`invitation/editUser/${user.id}`);
redirectToPage();
}

const _userAccessActionsFns = useUserAccessManagerActions();

function getItemActions(user) {
return _userAccessActionsFns.getItemActions(user);
}

function sendEmail({user}) {
_userAccessActionsFns.sendEmail({user}, triggerDataChangeCallback);
}

function disableUser({user}) {
_userAccessActionsFns.disableUser({user}, triggerDataChangeCallback);
}

function removeUser({user}) {
_userAccessActionsFns.removeUser({user}, triggerDataChangeCallback);
}

function mergeUser({user}) {
_userAccessActionsFns.mergeUser({user}, triggerDataChangeCallback);
}

function loginAs({user}) {
_userAccessActionsFns.loginAs({user});
}

return {
userAccessCount,
setCurrentPage,
currentPage,
userAccessPagination,
userList,
isUserAccessLoading,
setSearchPhrase,
sendEmail,
disableUser,
removeUser,
mergeUser,
loginAs,
getItemActions,
editUser,
getColumns,
};
},
);
Loading