Skip to content

Commit

Permalink
pkp/pkp-lib#9658 create invitation (#362)
Browse files Browse the repository at this point in the history
* send and accept invitation

* improved creating invitation workflow structure

* send invitation

* Clean Documentation

* Change ORCiD Id to ORCID id

* remove modal

---------

Co-authored-by: Jarda Kotěšovec <[email protected]>
Co-authored-by: withanage <[email protected]>
  • Loading branch information
3 people authored Jun 13, 2024
1 parent 655e37f commit 8807079
Show file tree
Hide file tree
Showing 15 changed files with 1,586 additions and 2 deletions.
15 changes: 15 additions & 0 deletions public/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,21 @@ window.pkp = {
'submission.upload.percentComplete': 'Uploading {$percent}% complete',
'submissions.incomplete': 'Incomplete',
'validator.required': 'This field is required.',
'invitation.notification.title': 'Invitation sent',
'invitation.wizard.success': "{$email} has been invited to a new role in OJS. You can be updated about the user's decision on the User & Role page, your OJS notification and/or your email",
'user.email': 'Email',
'user.username': 'Username',
'user.orcid': 'ORCiD ID',
'invitation.notification.closeBtn':'View all users',
'user.password': 'Password',
'invitation.role.selectRole':'Select a new role',
'invitation.role.dateEnd' : 'End Date',
'invitation.role.dateStart' : 'Start Date',
'invitation.role.masthead' : 'Journal Masthead',
'invitation.role.removeRole.button' : 'Remove Role',
'invitation.role.addRole.button':'Add Another Role',
'invitation.orcid.message':'Add Another Role',

},

tinyMCE: {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Container/PageOJS.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script>
import Page from '@/components/Container/Page.vue';
import SubmissionsPage from '@/pages/submissions/SubmissionsPage.vue';
import UserInvitationPage from '@/pages/userInvitation/UserInvitationPage.vue';
export default {
components: {
SubmissionsPage,
UserInvitationPage,
},
extends: Page,
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/mocks/field-text-orcid.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export default {
name: 'orcid',
component: 'field-text',
inputType: 'url',
label: 'ORCID iD',
label: 'ORCiD ID',
groupId: 'profile',
value: '',
};
54 changes: 53 additions & 1 deletion src/composables/useForm.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import {ref} from 'vue';
import {ref, watch} from 'vue';

function getField(form, name) {
const fields = form.fields;

return fields.find((field) => field.name === name);
}

function doesFieldExist(form, name) {
const fields = form.fields;

return !!fields.find((field) => field.name === name);
}

function getClearValue(field, localeKey = null) {
if (localeKey) {
if (field.component === 'field-slider') {
Expand All @@ -27,6 +33,48 @@ function mapFromSelectedToValue(selected) {
export function useForm(_form) {
const form = ref(_form);

function connectWithPayload(payload) {
watch(
payload,
(newPayload) => {
Object.keys(newPayload).forEach((key) => {
if (doesFieldExist(form.value, key)) {
if (getValue(key) !== newPayload[key]) {
setValue(key, newPayload[key]);
}
}
});
},
{immediate: true},
);
}

function connectWithErrors(errors) {
watch(
errors,
(newErrors) => {
Object.keys(newErrors).forEach((key) => {
if (doesFieldExist(form.value, key)) {
form.value.errors[key] = newErrors[key];
}
});
},
{immediate: true},
);
}

function set(key, data) {
Object.keys(data).forEach(function (dataKey) {
form.value[dataKey] = data[dataKey];
});
}

function getValue(name) {
const field = getField(form.value, name);

return field.value;
}

function setValue(name, inputValue) {
const field = getField(form.value, name);
if (!field) {
Expand Down Expand Up @@ -69,8 +117,12 @@ export function useForm(_form) {
}

return {
set,
setValue,
getValue,
clearForm,
form,
connectWithPayload,
connectWithErrors,
};
}
114 changes: 114 additions & 0 deletions src/pages/userInvitation/UserInvitationDetailsFormStep.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<template>
<div v-if="store.invitationPayload.userId === null">
<pkp-form
v-bind="userForm"
class="userInvitation__stepForm"
@set="updateUserForm"
></pkp-form>
</div>
<div v-if="store.invitationPayload.userId !== null">
<div class="userInvitation__reviewPanel__item">
<h4 class="userInvitation__reviewPanel__item__header">Email Address</h4>
<div class="userInvitation__reviewPanel__item__value">
{{ store.invitationPayload.email }}
</div>
<h4 class="userInvitation__reviewPanel__item__header">ORCID iD</h4>
<div class="userInvitation__reviewPanel__item__value">
{{
store.invitationPayload.orcid
? store.invitationPayload.orcid
: t('invitation.orcid.message')
}}
<icon
v-if="store.invitationPayload.orcidValidation"
icon="orcid"
:inline="true"
/>
</div>
<h4 class="userInvitation__reviewPanel__item__header">Given Name</h4>
<div class="userInvitation__reviewPanel__item__value">
{{ store.invitationPayload.givenName }}
</div>
<h4 class="userInvitation__reviewPanel__item__header">Family Name</h4>
<div class="userInvitation__reviewPanel__item__value">
{{ store.invitationPayload.familyName }}
</div>
<h4 class="userInvitation__reviewPanel__item__header">Affiliation</h4>
<div class="userInvitation__reviewPanel__item__value">
{{ store.invitationPayload.affiliation }}
</div>
</div>
</div>
<br />
<UserInvitationUserGroupsTable
:user-groups="userGroups"
:errors="sectionErrors"
/>
</template>

<script setup>
import {defineProps, computed} from 'vue';
import PkpForm from '@/components/Form/Form.vue';
import {useTranslation} from '@/composables/useTranslation';
import UserInvitationUserGroupsTable from './UserInvitationUserGroupsTable.vue';
import {useUserInvitationPageStore} from './UserInvitationPageStore';
import {useForm} from '@/composables/useForm';
function updateUserForm(a, {fields}, c, d) {
if (fields) {
fields.forEach((field) => {
if (store.invitationPayload[field.name] !== field.value) {
store.updatePayload(field.name, field.value);
}
});
}
}
const {t} = useTranslation();
const store = useUserInvitationPageStore();
const props = defineProps({
form: {type: Object, required: true},
userGroups: {type: Object, required: true},
validateFields: {type: Array, required: true},
});
const {
form: userForm,
connectWithPayload,
connectWithErrors,
} = useForm(props.form);
connectWithPayload(store.invitationPayload);
const sectionErrors = computed(() => {
return props.validateFields.reduce((obj, key) => {
if (store.errors[key]) {
obj[key] = store.errors[key];
}
return obj;
}, {});
});
connectWithErrors(sectionErrors);
</script>
<style lang="less">
select {
width: 13rem !important;
}
.userInvitation__reviewPanel__item {
&:last-child {
border-bottom: none;
}
}
.userInvitation__reviewPanel__item__header {
margin: 0;
font-size: 0.875rem;
line-height: 1.5rem;
}
.userInvitation__reviewPanel__item__value {
margin-bottom: 1rem;
font-size: 0.875rem;
line-height: 1.5rem;
}
</style>
89 changes: 89 additions & 0 deletions src/pages/userInvitation/UserInvitationEmailComposerStep.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<composer
:id="store.currentStep.id"
:add-c-c-label="t('common.addCCBCC')"
:attach-files-label="t('common.attachFiles')"
:attached-files-label="t('common.attachedFiles')"
:attachers="props.email.attachers"
:attachments="props.email.attachments"
:bcc="props.email.bcc"
:bcc-label="t('email.bcc')"
:body-label="t('stageParticipants.notify.message')"
:can-change-recipients="props.email.canChangeRecipients"
:cc="props.email.cc"
:cc-label="t('email.cc')"
:confirm-switch-locale-label="t('email.confirmSwitchLocale')"
:deselect-label="t('common.deselect')"
:email-templates="props.email.emailTemplates"
:email-templates-api-url="store.emailTemplatesApiUrl"
:errors="sectionErrors.emailComposer"
:find-template-label="t('common.findTemplate')"
:initial-template-key="props.email.initialTemplateKey"
:insert-label="t('common.insert')"
:insert-modal-label="t('common.insertContent')"
:insert-content-label="t('common.content')"
:insert-search-label="t('common.insertContentSearch')"
:load-template-label="t('common.emailTemplates')"
:locale="props.email.locale"
:locales="props.email.locales"
:more-search-results-label="t('common.numberedMore')"
:recipient-options="props.email.recipientOptions"
:recipients="props.email.recipients"
:recipients-label="t('email.to')"
:remove-item-label="t('common.removeItem')"
:searching-label="t('common.searching')"
:search-results-label="t('search.searchResults')"
:subject-label="t('email.subject')"
:switch-to-label="t('common.switchTo')"
:switch-to-named-language-label="t('common.switchToNamedItem')"
:variables="props.email.variables"
v-bind="emailComposer"
@set="(componentName, update) => updateEmail(update)"
></composer>
</template>

<script setup>
import {computed} from 'vue';
import Composer from '@/components/Composer/Composer.vue';
import {useTranslation} from '@/composables/useTranslation';
import {defineProps} from 'vue';
import {useUserInvitationPageStore} from './UserInvitationPageStore';
const props = defineProps({
email: {type: Object, required: true},
validateFields: {
type: Array,
default() {
return null;
},
},
});
const {t} = useTranslation();
const store = useUserInvitationPageStore();
const emailComposer = computed(() => store.invitationPayload.emailComposer);
function updateEmail(update) {
const emailComposerUpdate = {
...store.invitationPayload.emailComposer,
...update,
};
store.updatePayload('emailComposer', emailComposerUpdate);
}
if (!store.invitationPayload.body) {
updateEmail({
subject: props.email.subject,
body: props.email.body,
});
}
const sectionErrors = computed(() => {
return props.validateFields.reduce((obj, key) => {
if (store.errors[key]) {
obj[key] = store.errors[key];
}
return obj;
}, {});
});
</script>
17 changes: 17 additions & 0 deletions src/pages/userInvitation/UserInvitationHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<h1 ref="wrapper" class="app__pageHeading">
{{ pageTitle }}
</h1>
<p>
{{ pageTitleDescription }}
</p>
</template>

<script setup>
import {defineProps} from 'vue';
defineProps({
pageTitle: {type: String, required: true},
pageTitleDescription: {type: String, required: true},
});
</script>
9 changes: 9 additions & 0 deletions src/pages/userInvitation/UserInvitationPage.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 UserInvitationPage from './UserInvitationPage.stories.js';

<Meta of={UserInvitationPage} />

# User Invitation page

<ArgTypes />
Loading

0 comments on commit 8807079

Please sign in to comment.