Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions public/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,14 @@ window.pkp = {
"Once the user is enabled, they will regain access to OJS, and you'll be able to invite them to roles as needed.",
'grid.user.grid.user.disableReasonDescription':
"Please note that once a user is disabled, you won't be able to add them to any roles until they are enabled again.",
'reviewerInvitation.responseDueDate':'Review Response Date',
'reviewerInvitation.reviewDueDate':'Review Response Due Date',
'reviewerInvitation.reviewTypes':'Review Types',
'reviewerInvitation.modal.message':'{$email} has been invited to review the submission "{$articleTitle}"<br><br> You can be updated about the user\'s descision on the reviewer panel in the review workflow or through email and OJS notifications',
'reviewerInvitation.modal.button':'View submission',
'reviewerInvitation.reviewTypes.anonymusAuthorOrReviewer':'Anonymus Reviewer / Anonymus Author',
'reviewerInvitation.reviewTypes.disclosedAuthor':'Anonymus Reviewer / Disclosed Author',
'reviewerInvitation.reviewTypes.open':'Open'
},
tinyMCE: {
skinUrl: '/styles/tinymce',
Expand Down
14 changes: 12 additions & 2 deletions src/components/ListPanel/users/SelectReviewerListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ import ListItem from '@/components/List/ListItem.vue';
import Badge from '@/components/Badge/Badge.vue';
import PkpButton from '@/components/Button/Button.vue';
import Icon from '@/components/Icon/Icon.vue';
import {useWorkflowStore} from '@/pages/workflow/workflowStore';
import {useUrl} from '@/composables/useUrl';

export default {
components: {
Expand Down Expand Up @@ -482,8 +484,16 @@ export default {
* @param
*/
select() {
this.$emit('select', this.item);
pkp.eventBus.$emit('selected:reviewer', this.item);
const workflow = useWorkflowStore();
const {redirectToPage: redirectToReviewerInvitationPage} = useUrl(
'invitation/create/reviewerAccess',
{
userId: this.item.id,
submissionId: workflow.submission.id,
reviewRoundId: workflow.selectedMenuState.reviewRoundId,
},
);
redirectToReviewerInvitationPage();
},

/**
Expand Down
84 changes: 84 additions & 0 deletions src/pages/userInvitation/ReviewerReviewDetailsStep.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<FieldOptions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please do this using the Form component?
When possible we still want to stick with forms as much as possible. Cherry-picking form component is currently used only for unusual scenarios, like having fields in the table etc.. And this looks like good fit for form.

Good news is that you don't have to build this one server side anymore. useForm now has bunch of addField* method to support some basic form fields. You can see example of using that for example in useDiscussionManagerForm.

:is-required="false"
component="field-options"
name="reviewTypes"
:label="t('reviewerInvitation.reviewTypes')"
type="radio"
:options="options"
:all-errors="sectionErrors"
@change="
(fieldName, propName, newValue, localeKey) =>
updateReviewDetails(index, fieldName, newValue)
"
/>
<div class="p-8">
<FieldText
name="responseDueDate"
:label="t('reviewerInvitation.responseDueDate')"
input-type="date"
:is-required="true"
value=""
:all-errors="sectionErrors"
@change="
(fieldName, propName, newValue, localeKey) =>
updateReviewDetails(index, fieldName, newValue)
"
/>
<FieldText
name="reviewDueDate"
:label="t('reviewerInvitation.reviewDueDate')"
input-type="date"
:is-required="true"
value=""
:all-errors="sectionErrors"
@change="
(fieldName, propName, newValue, localeKey) =>
updateReviewDetails(index, fieldName, newValue)
"
/>
</div>
</template>
<script setup>
import {defineProps, computed} from 'vue';
import FieldOptions from '@/components/Form/fields/FieldOptions.vue';
import FieldText from '@/components/Form/fields/FieldText.vue';
import {useLocalize} from '@/composables/useLocalize';
import {useUserInvitationPageStore} from './UserInvitationPageStore';

const props = defineProps({
validateFields: {type: Array, required: true},
});

const store = useUserInvitationPageStore();
const {t} = useLocalize();
const options = [
{
value: 'anonymus',
label: t('reviewerInvitation.reviewTypes.anonymusAuthorOrReviewer'),
},
{
value: 'disclosed',
label: t('reviewerInvitation.reviewTypes.disclosedAuthor'),
},
{value: 'open', label: t('reviewerInvitation.reviewTypes.open')},
];

props.validateFields.forEach((field) => {
store.updatePayload(field, '', false);
});

function updateReviewDetails(index, fieldName, newValue) {
delete store.errors[fieldName];
store.updatePayload(fieldName, newValue, false);
}

const sectionErrors = computed(() => {
return props.validateFields.reduce((obj, key) => {
if (store.errors[key]) {
obj[key] = store.errors[key];
}
return obj;
}, {});
});
</script>
98 changes: 98 additions & 0 deletions src/pages/userInvitation/UserInvitationPage.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import UserInvitationPage from './UserInvitationPage.vue';
import {http, HttpResponse} from 'msw';
import userMock from './mocks/userMock.js';
import PageInitConfigMock from './mocks/pageInitConfig';
import ReviewerPageInitConfigMock from './mocks/reviewerPageInitConfig';

export default {title: 'Pages/UserInvitation', component: UserInvitationPage};

Expand Down Expand Up @@ -118,3 +119,100 @@ export const Init = {
},
args: PageInitConfigMock,
};

export const Reviewer = {
render: (args) => ({
components: {UserInvitationPage},
setup() {
return {args};
},
template: '<UserInvitationPage v-bind="args" />',
}),
parameters: {
msw: {
handlers: [
http.post(
'https://mock/index.php/publicknowledge/api/v1/invitations/add/reviewerAccessInvite',
() => {
return HttpResponse.json({invitationId: 15});
},
),
http.post(
'https://mock/index.php/publicknowledge/api/v1/invitations/15/populate',
async ({request}) => {
const data = await request.json();
let errors = {};
console.log(data.invitationData);

data.invitationData.userGroupsToAdd.forEach((element, index) => {
Object.keys(element).forEach((key) => {
if (element[key] === null) {
errors = {
...errors,
['userGroupsToAdd.' + index + '.' + key]: [
'This field is required',
],
};
}
});
});

if (data.invitationData.email === '') {
errors['email'] = ['This field is required'];
}
if (data.invitationData.familyName === '') {
errors['familyName'] = ['This field is required'];
}
if (data.invitationData.givenName === '') {
errors['givenName'] = ['This field is required'];
}

if (data.invitationData.responseDueDate === '') {
errors['responseDueDate'] = ['This field is required'];
}

if (data.invitationData.reviewDueDate === '') {
errors['reviewDueDate'] = ['This field is required'];
}

if (data.invitationData.reviewTypes === '') {
errors['reviewTypes'] = ['This field is required'];
}

if (data.invitationData.emailComposer) {
Object.keys(data.invitationData.emailComposer).forEach(
(element) => {
if (data.invitationData.emailComposer[element] === '') {
errors['emailComposer'] = {
...errors['emailComposer'],
[element]: ['This field is required'],
};
}
},
);
}

if (Object.keys(errors).length > 0) {
return HttpResponse.json({errors: errors}, {status: 422});
}

return HttpResponse.json({status: 201});
},
),
http.post(
'https://mock/index.php/publicknowledge/api/v1/invitations/15/invite',
() => {
return HttpResponse.json({});
},
),
http.post(
'https://mock/index.php/publicknowledge/api/v1/user/_invite',
() => {
return HttpResponse.json('invitation send successfully');
},
),
],
},
},
args: ReviewerPageInitConfigMock,
};
3 changes: 3 additions & 0 deletions src/pages/userInvitation/UserInvitationPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import UserInvitationHeader from './UserInvitationHeader.vue';
import UserInvitationDetailsFormStep from './UserInvitationDetailsFormStep.vue';
import UserInvitationSearchFormStep from './UserInvitationSearchFormStep.vue';
import UserInvitationEmailComposerStep from './UserInvitationEmailComposerStep.vue';
import ReviewerReviewDetailsStep from './ReviewerReviewDetailsStep.vue';

const props = defineProps({
/** steps for invite user */
Expand Down Expand Up @@ -140,5 +141,7 @@ const userInvitationComponents = {
UserInvitationDetailsFormStep,
UserInvitationSearchFormStep,
UserInvitationEmailComposerStep,
ReviewerReviewDetailsStep,
};
console.log(props);
</script>
38 changes: 33 additions & 5 deletions src/pages/userInvitation/UserInvitationPageStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {useModal} from '@/composables/useModal';
export const useUserInvitationPageStore = defineComponentStore(
'userInvitationPage',
(pageInitConfig) => {
const invitationUserRoleAssignment = 'userRoleAssignment';
const invitationReviewerAccessInvite = 'reviewerAccess';
const {openDialog} = useModal();
const {t} = useLocalize();
/**
Expand All @@ -26,6 +28,13 @@ export const useUserInvitationPageStore = defineComponentStore(
const invitationUserData = ref(pageInitConfig.invitationUserData);
const detectChanges = ref(false);

invitationPayload.value.submissionId &&
(updatedPayload.value['submissionId'] =
invitationPayload.value.submissionId);
invitationPayload.value.reviewRoundId &&
(updatedPayload.value['reviewRoundId'] =
invitationPayload.value.reviewRoundId);

function updatePayload(fieldName, value, initialValue = true) {
invitationPayload.value[fieldName] = value;
if (!initialValue) {
Expand Down Expand Up @@ -76,6 +85,14 @@ export const useUserInvitationPageStore = defineComponentStore(
return currentStepIndex.value === steps.value.length - 1;
});

const isUserRoleAssignment = computed(() => {
return invitationType.value === invitationUserRoleAssignment;
});

const isReviewerAccess = computed(() => {
return invitationType.value === invitationReviewerAccessInvite;
});

/**
* Add a step change to the browser history so the
* user can use the browser's back button
Expand Down Expand Up @@ -317,7 +334,7 @@ export const useUserInvitationPageStore = defineComponentStore(
async (newVal, oldVal) => {
isSubmitting.value = invitationPayload.value.disabled;
if (invitationPayload.value.userGroupsToAdd.length === 0) {
isSubmitting.value = true;
// isSubmitting.value = true;
}
detectChanges.value = true;
},
Expand All @@ -339,12 +356,21 @@ export const useUserInvitationPageStore = defineComponentStore(
if (data.value) {
openDialog({
title: t('userInvitation.modal.title'),
message: t('userInvitation.modal.message', {
email: invitationPayload.value.inviteeEmail,
}),
message:
invitationType.value === invitationUserRoleAssignment
? t('userInvitation.modal.message', {
email: invitationPayload.value.inviteeEmail,
})
: t('reviewerInvitation.modal.message', {
email: invitationPayload.value.inviteeEmail,
articleTitle: '',
}),
actions: [
{
label: t('userInvitation.modal.button'),
label:
invitationType.value === invitationUserRoleAssignment
? t('userInvitation.modal.button')
: t('reviewerInvitation.modal.button'),
callback: (close) => {
redirectToPage();
},
Expand Down Expand Up @@ -413,6 +439,8 @@ export const useUserInvitationPageStore = defineComponentStore(
registerActionForStepId,
emailTemplatesApiUrl,
invitationUserData,
isUserRoleAssignment,
isReviewerAccess,

currentStep,
currentStepIndex,
Expand Down
28 changes: 24 additions & 4 deletions src/pages/userInvitation/UserInvitationUserGroupsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,24 @@
</div>
</TableCell>
</TableRow>
<template v-if="!store.invitationPayload.disabled">
<template
v-if="
!store.invitationPayload.disabled &&
!(
store.invitationPayload.currentUserGroups.some((group) =>
reviewerUserGroupIds.includes(group.id),
) && store.isReviewerAccess
)
"
>
<TableRow
v-for="(userGroupToAdd, index) in allUserGroupsToAdd"
:key="index"
>
<TableCell>
<FieldSelect
name="userGroupId"
:disabled="store.isReviewerAccess"
:label="t('invitation.role.selectRole')"
:is-required="true"
:value="userGroupToAdd.userGroupId"
Expand Down Expand Up @@ -119,8 +129,9 @@
<TableCell>
<PkpButton
v-if="
store.invitationPayload.userGroupsToAdd.length > 1 ||
isUserGroupsToAddPopulated()
(store.invitationPayload.userGroupsToAdd.length > 1 ||
isUserGroupsToAddPopulated()) &&
store.isUserRoleAssignment
"
:is-warnable="true"
@click="removeInvitedUserGroup(index)"
Expand All @@ -131,7 +142,7 @@
</TableRow>
</template>
</TableBody>
<template #bottom-controls>
<template v-if="store.isUserRoleAssignment" #bottom-controls>
<PkpButton
:is-disabled="store.invitationPayload.disabled"
@click="addUserGroup()"
Expand Down Expand Up @@ -362,4 +373,13 @@ async function removeRole(userId, roleId) {
});
await fetch();
}

if (
store.invitationPayload.currentUserGroups.some((group) =>
reviewerUserGroupIds.value.includes(group.id),
) &&
store.isReviewerAccess
) {
store.updatePayload('userGroupsToAdd', [], false);
}
</script>
Loading