From 4e531b00e27c5d3af07535e1defaf65b1c020382 Mon Sep 17 00:00:00 2001
From: ipula
Date: Thu, 21 Mar 2024 19:42:59 +0100
Subject: [PATCH] send and accept invitation
---
public/globals.js | 7 +
src/components/Container/PageOJS.vue | 4 +
.../AcceptInvitationCreateUserAccount.vue | 24 +
.../AcceptInvitationCreateUserForms.vue | 20 +
.../AcceptInvitationHeader.vue | 17 +
.../acceptInvitation/AcceptInvitationPage.mdx | 9 +
.../AcceptInvitationPage.stories.js | 28 +
.../acceptInvitation/AcceptInvitationPage.vue | 405 +++++++++++++
.../AcceptInvitationPageStore.js | 543 ++++++++++++++++++
.../AcceptInvitationReview.vue | 256 +++++++++
.../AcceptInvitationUserRoles.vue | 29 +
.../AcceptInvitationVerifyOrcid.vue | 11 +
.../acceptInvitation/mocks/pageInitConfig.js | 204 +++++++
.../UserInvitationDetailsFormStep.vue | 20 +
.../UserInvitationEmailComposerStep.vue | 57 ++
.../userInvitation/UserInvitationHeader.vue | 17 +
.../userInvitation/UserInvitationPage.mdx | 9 +
.../UserInvitationPage.stories.js | 32 ++
.../userInvitation/UserInvitationPage.vue | 421 ++++++++++++++
.../userInvitation/UserInvitationPageStore.js | 479 +++++++++++++++
.../UserInvitationSearchFormStep.vue | 27 +
.../userInvitation/mocks/pageInitConfig.js | 223 +++++++
src/pages/userInvitation/mocks/userMock.js | 67 +++
23 files changed, 2909 insertions(+)
create mode 100644 src/pages/acceptInvitation/AcceptInvitationCreateUserAccount.vue
create mode 100644 src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue
create mode 100644 src/pages/acceptInvitation/AcceptInvitationHeader.vue
create mode 100644 src/pages/acceptInvitation/AcceptInvitationPage.mdx
create mode 100644 src/pages/acceptInvitation/AcceptInvitationPage.stories.js
create mode 100644 src/pages/acceptInvitation/AcceptInvitationPage.vue
create mode 100644 src/pages/acceptInvitation/AcceptInvitationPageStore.js
create mode 100644 src/pages/acceptInvitation/AcceptInvitationReview.vue
create mode 100644 src/pages/acceptInvitation/AcceptInvitationUserRoles.vue
create mode 100644 src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue
create mode 100644 src/pages/acceptInvitation/mocks/pageInitConfig.js
create mode 100644 src/pages/userInvitation/UserInvitationDetailsFormStep.vue
create mode 100644 src/pages/userInvitation/UserInvitationEmailComposerStep.vue
create mode 100644 src/pages/userInvitation/UserInvitationHeader.vue
create mode 100644 src/pages/userInvitation/UserInvitationPage.mdx
create mode 100644 src/pages/userInvitation/UserInvitationPage.stories.js
create mode 100644 src/pages/userInvitation/UserInvitationPage.vue
create mode 100644 src/pages/userInvitation/UserInvitationPageStore.js
create mode 100644 src/pages/userInvitation/UserInvitationSearchFormStep.vue
create mode 100644 src/pages/userInvitation/mocks/pageInitConfig.js
create mode 100644 src/pages/userInvitation/mocks/userMock.js
diff --git a/public/globals.js b/public/globals.js
index 0bc3d1468..b9e3229db 100644
--- a/public/globals.js
+++ b/public/globals.js
@@ -276,6 +276,13 @@ window.pkp = {
'submission.submit.newSubmissionSingle': 'New Submission',
'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',
},
tinyMCE: {
skinUrl: '/styles/tinymce',
diff --git a/src/components/Container/PageOJS.vue b/src/components/Container/PageOJS.vue
index 2b034b21e..f13455ad5 100644
--- a/src/components/Container/PageOJS.vue
+++ b/src/components/Container/PageOJS.vue
@@ -1,10 +1,14 @@
diff --git a/src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue b/src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue
new file mode 100644
index 000000000..a96b44ede
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationHeader.vue b/src/pages/acceptInvitation/AcceptInvitationHeader.vue
new file mode 100644
index 000000000..f67629358
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationHeader.vue
@@ -0,0 +1,17 @@
+
+
+ {{ pageTitle }}
+
+
+ {{ pageTitleDescription }}
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationPage.mdx b/src/pages/acceptInvitation/AcceptInvitationPage.mdx
new file mode 100644
index 000000000..6e6766ee3
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPage.mdx
@@ -0,0 +1,9 @@
+import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';
+
+import * as AcceptInvitationPage from './AcceptInvitationPage.stories.js';
+
+
+
+# User Invitation page
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationPage.stories.js b/src/pages/acceptInvitation/AcceptInvitationPage.stories.js
new file mode 100644
index 000000000..5a5662db4
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPage.stories.js
@@ -0,0 +1,28 @@
+import AcceptInvitationPage from './AcceptInvitationPage.vue';
+import {http, HttpResponse} from 'msw';
+import PageInitConfigMock from './mocks/pageInitConfig';
+
+export default {
+ title: 'Pages/AcceptInvitation',
+ component: AcceptInvitationPage,
+};
+
+export const Init = {
+ render: (args) => ({
+ components: {AcceptInvitationPage},
+ setup() {
+ return {args};
+ },
+ template: '',
+ }),
+ parameters: {
+ msw: {
+ handlers: [
+ http.post('https://mock/index.php/publicknowledge/api/v1/users', () => {
+ return HttpResponse.json('accept invitation successfully');
+ }),
+ ],
+ },
+ },
+ args: PageInitConfigMock,
+};
diff --git a/src/pages/acceptInvitation/AcceptInvitationPage.vue b/src/pages/acceptInvitation/AcceptInvitationPage.vue
new file mode 100644
index 000000000..7fc28e931
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPage.vue
@@ -0,0 +1,405 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('invitation.wizard.errors') }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('invitation.wizard.success', {email: store.email}) }}
+
+
+ {{ t('invitation.notification.closeBtn') }}
+
+
+
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationPageStore.js b/src/pages/acceptInvitation/AcceptInvitationPageStore.js
new file mode 100644
index 000000000..a12db6dcf
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPageStore.js
@@ -0,0 +1,543 @@
+import {useTranslation} from '@/composables/useTranslation';
+
+import {defineComponentStore} from '@/utils/defineComponentStore';
+import {useFetch} from '@/composables/useFetch';
+import {computed, onMounted, ref, watch} from 'vue';
+//let pageInitConfig = null;
+
+/*export function initSubmissionsPageStore(_pageInitConfig) {
+ pageInitConfig = _pageInitConfig;
+}
+
+export function disposeSubmissionsPageStore() {
+ const store = useSubmissionsPageStore();
+ store.$dispose();
+ pageInitConfig = null;
+ delete getActivePinia().state.value[store.$id];
+}*/
+
+export const useAcceptInvitationPageStore = defineComponentStore(
+ 'userInvitationPage',
+ (pageInitConfig) => {
+ /**
+ * Translation
+ */
+
+ const {t} = useTranslation();
+
+ const currentStepId = ref(pageInitConfig.steps[0].id);
+ const steps = ref(pageInitConfig.steps);
+ const pageTitleDescription = ref(pageInitConfig.pageTitleDescription);
+ const errors = ref({});
+ const startedSteps = ref([]);
+ const isModalOpened = ref(false);
+ const reviewSteps = ref([]);
+
+ const emailField = ref({
+ label: t('user.email'),
+ name: 'email',
+ size: 'large',
+ value: '',
+ allErrors: {},
+ });
+
+ const usernameField = ref({
+ label: t('user.username'),
+ name: 'username',
+ size: 'large',
+ value: '',
+ allErrors: {},
+ });
+ const passwordField = ref({
+ label: t('user.password'),
+ name: 'password',
+ size: 'large',
+ value: '',
+ inputType: 'password',
+ allErrors: {},
+ });
+ const password = ref('');
+ const username = ref('');
+ const user = ref('');
+ const email = ref(pageInitConfig.userEmail);
+ emailField.value.value = email;
+
+ /**
+ * The currently active step
+ */
+ const currentStep = computed(() => {
+ return steps.value.find((step) => step.id === currentStepId.value);
+ });
+
+ /**
+ * The index of the currently active step
+ * in the steps array
+ */
+ const currentStepIndex = computed(() => {
+ return steps.value.findIndex((step) => step.id === currentStepId.value);
+ });
+
+ /**
+ * Is the current step the first step?
+ */
+ const isOnFirstStep = computed(() => {
+ return !currentStepIndex.value;
+ });
+
+ /**
+ * Is the current step the last step?
+ */
+ const isOnLastStep = computed(() => {
+ return currentStepIndex.value === steps.value.length - 1;
+ });
+
+ /**
+ * Are there any validation errors?
+ */
+ const isValid = computed(() => {
+ return Object.keys(errors.value).length === 0;
+ });
+ /**
+ * The title to show at the top of the page
+ */
+ const pageTitle = computed(() => {
+ if (!currentStep.value) {
+ return '';
+ }
+ return currentStep.value.name.replace('{$step}', currentStep.value);
+ });
+
+ /**
+ * The step title to show at the top of the step
+ */
+ const stepTitle = computed(() => {
+ if (!currentStep.value) {
+ return '';
+ }
+ return currentStep.value.stepName.replace(
+ '{$step}',
+ 'STEP ' + (1 + currentStepIndex.value),
+ );
+ });
+
+ /**
+ * Create request body
+ */
+ const userCreateRequestBody = computed(() => {
+ const reqBody = {
+ username: username.value,
+ password: password.value,
+ email: email.value,
+ invitationId: pageInitConfig.invitationId,
+ invitationKey: pageInitConfig.invitationKey,
+ };
+
+ return reqBody;
+ });
+
+ /**
+ *
+ */
+ const updateUserRequestBody = computed(() => {
+ const reqBody = {
+ username: username.value,
+ password: password.value,
+ email: email.value,
+ invitationId: pageInitConfig.invitationId,
+ invitationKey: pageInitConfig.invitationKey,
+ };
+ steps.value.forEach((step) => {
+ step.sections.forEach((section) => {
+ if (section.type === 'form' || section.length > 0) {
+ section.form.fields.forEach((field) => {
+ reqBody[field.name] = field.value;
+ });
+ }
+ });
+ });
+
+ return reqBody;
+ });
+
+ /**
+ * Update when the step changes
+ */
+ watch(currentStepIndex, async (newVal, oldVal) => {
+ if (newVal === oldVal) {
+ return;
+ }
+
+ // Update the list of steps that have been started
+ steps.value.forEach((step, i) => {
+ if (
+ !startedSteps.value.includes(step.id) &&
+ i <= currentStepIndex.value
+ ) {
+ startedSteps.value.push(step.id);
+ }
+ });
+
+ // Track step changes in the title and browser history
+ const step = steps.value[newVal];
+ // document.title = this.getPageTitle(step);
+ if (step.id !== window.location.hash.replace('#', '')) {
+ addHistory(step);
+ }
+
+ // Trigger validation on the review step
+ if (newVal === steps.value.length - 1) {
+ validateOnServer(steps.value[currentStepIndex.value].id);
+ }
+ });
+
+ /**
+ * Set form data when validation errors are changed
+ */
+ watch(errors, async (newVal, oldVal) => {
+ const keys = Object.keys(newVal);
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type === 'form') {
+ section.form.fields.forEach((field) => {
+ if (keys.includes(field.name)) {
+ steps.value[stepIndex].sections[sectionIndex].form.errors = {
+ ...steps.value[stepIndex].sections[sectionIndex].form.errors,
+ ...{[field.name]: newVal[field.name]},
+ };
+ }
+ });
+ }
+ });
+ });
+ });
+
+ onMounted(() => {
+ /**
+ * Open the correct step when the page is loaded
+ */
+ if (!window.location.hash) {
+ openStep(steps.value[0].id);
+ }
+ });
+
+ function passwordChange(fieldName, propName, newValue, localeKey) {
+ password.value = newValue;
+ passwordField.value.value = newValue;
+ passwordField.value.allErrors = {};
+ }
+ function usernameChange(fieldName, propName, newValue, localeKey) {
+ username.value = newValue;
+ usernameField.value.value = newValue;
+ usernameField.value.allErrors = {};
+ }
+
+ function emailChange(fieldName, propName, newValue, localeKey) {
+ email.value = newValue;
+ email.value.value = newValue;
+ email.value.allErrors = {};
+ }
+ /**
+ * Add a step change to the browser history so the
+ * user can use the browser's back button
+ *
+ * @param {Object} step The step to add
+ */
+ function addHistory(step) {
+ window.history.pushState({}, step.name, '#' + step.id);
+ }
+
+ /**
+ * Go to the next step or submit if this is the last step
+ */
+ function nextStep() {
+ if (!validateOnClient(steps.value[currentStepIndex.value].id)) {
+ return;
+ }
+ if (isOnFirstStep.value) {
+ createUser();
+ } else if (isOnLastStep.value) {
+ submit();
+ } else {
+ openStep(steps.value[1 + currentStepIndex.value].id);
+ }
+ }
+
+ /**
+ * Go to a step in the wizard
+ *
+ * @param {String} stepId
+ */
+ function openStep(stepId) {
+ const newStep = steps.value.find((step) => step.id === stepId);
+ if (!newStep) {
+ return;
+ }
+ currentStepId.value = stepId;
+ }
+
+ /**
+ * Go to the previous step in the wizard
+ */
+ function previousStep() {
+ const previousIndex = currentStepIndex.value - 1;
+ if (previousIndex >= 0) {
+ openStep(steps.value[previousIndex].id);
+ }
+ }
+
+ /**
+ * Validate the user details
+ *
+ * Always wait for autosaves to complete before
+ * trying to validate
+ */
+ function validateOnClient(stepId) {
+ let isValidate = true;
+ usernameField.value.allErrors = {};
+ passwordField.value.allErrors = {};
+ emailField.value.allErrors = {};
+ if (stepId === 'userCreate') {
+ if (username.value === '') {
+ usernameField.value.allErrors.username = [t('validator.required')];
+ isValidate = false;
+ }
+ if (password.value === '') {
+ passwordField.value.allErrors.password = [t('validator.required')];
+ isValidate = false;
+ }
+ if (email.value === '') {
+ emailField.value.allErrors.email = [t('validator.required')];
+ isValidate = false;
+ }
+ }
+ if (stepId === 'userDetails') {
+ const formErrors = {};
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type === 'form') {
+ section.form.fields.forEach((field) => {
+ if (
+ (field.value === '' || field.value === null) &&
+ field.isRequired
+ ) {
+ formErrors[field.name] = [t('validator.required')];
+ isValidate = false;
+ }
+ });
+ }
+ });
+ });
+ errors.value = formErrors;
+ }
+
+ return isValidate;
+ }
+ /**
+ * Validate the user details
+ *
+ * Always wait for autosaves to complete before
+ * trying to validate
+ */
+ async function validateOnServer(stepId) {
+ let isValidate = true;
+ updateUserRequestBody.value['_validateOnly'] = true;
+ const {
+ data: res,
+ validationError,
+ fetch,
+ } = useFetch(pageInitConfig.userApiUrl + '/' + user.value.id, {
+ expectValidationError: true,
+ method: 'PUT',
+ body: updateUserRequestBody.value,
+ });
+ await fetch();
+ if (validationError.value) {
+ isValidate = false;
+ errors.value = validationError.value;
+ } else if (res.value && !validationError.value) {
+ errors.value = {};
+ }
+ return isValidate;
+ }
+
+ /**
+ * Update an autosave form
+ *
+ * @param {String} formId
+ * @param {Object} data
+ */
+ function updateAutosaveForm(formId, data) {
+ updateForm(formId, data);
+ }
+
+ /**
+ * Complete the submission
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ async function createUser() {
+ const {
+ data: res,
+ validationError,
+ fetch,
+ } = useFetch(pageInitConfig.userApiUrl, {
+ expectValidationError: true,
+ method: 'POST',
+ body: userCreateRequestBody.value,
+ });
+ await fetch();
+ if (validationError.value) {
+ errors.value = validationError.value;
+ if (validationError.value.username) {
+ usernameField.value.allErrors.username =
+ validationError.value.username;
+ }
+ if (validationError.value.password) {
+ passwordField.value.allErrors.password =
+ validationError.value.password;
+ }
+ if (validationError.value.email) {
+ emailField.value.allErrors.email = validationError.value.email;
+ }
+ steps.value.forEach((step) => {
+ if (step.id !== 'userCreate') {
+ return;
+ }
+ step.reviewData[0] = {
+ username: username.value,
+ password: password.value,
+ email: email.value,
+ };
+ });
+ } else if (res.value && !validationError.value) {
+ errors.value = {};
+ user.value = res.value;
+ openStep(steps.value[1 + currentStepIndex.value].id);
+ }
+ }
+
+ /**
+ * Complete the submission
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ async function submit() {
+ delete updateUserRequestBody.value._validateOnly;
+ const {
+ data: res,
+ validationError,
+ fetch,
+ } = useFetch(pageInitConfig.userApiUrl + '/' + user.value.id, {
+ expectValidationError: true,
+ method: 'PUT',
+ body: updateUserRequestBody.value,
+ });
+ await fetch();
+ if (validationError.value) {
+ errors.value = validationError.value;
+ } else if (res.value && !validationError.value) {
+ errors.value = {};
+ isModalOpened.value = true;
+ }
+ }
+
+ /**
+ * Update a form with new data
+ *
+ * This is fired every time a form field changes, so
+ * resource-intensive code should not be run every
+ * time this method is called.
+ *
+ * @param {String} formId
+ * @param {Object} data
+ */
+ function updateForm(formId, data) {
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type !== 'form' || section.form.id !== formId) {
+ return;
+ }
+ steps.value[stepIndex].sections[sectionIndex].form = {
+ ...steps.value[stepIndex].sections[sectionIndex].form,
+ ...data,
+ };
+ });
+ });
+ }
+ /**
+ * Complete the submission
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ // async function searchUser() {
+ // }
+
+ /**
+ * Update the data attached to a step
+ *
+ * @param {String} stepId The id of the step to update
+ * @param {Object} data The data to update in the step
+ */
+ function updateStep(stepId, data) {
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type !== 'email') {
+ return;
+ }
+ let errors = {...step.sections.errors};
+ Object.keys(data).forEach((key) => delete errors[key]);
+ steps.value[stepIndex].sections[sectionIndex].email = {
+ ...steps.value[stepIndex].sections[sectionIndex].email,
+ ...data,
+ errors: errors,
+ };
+ });
+ });
+ }
+
+ /**
+ * Close modal popup
+ */
+ function isModalClosed() {
+ isModalOpened.value = false;
+ }
+
+ return {
+ //computed
+ currentStep,
+ currentStepIndex,
+ isOnFirstStep,
+ isOnLastStep,
+ isValid,
+ pageTitle,
+ startedSteps,
+ stepTitle,
+ openStep,
+ steps,
+ pageTitleDescription,
+ errors,
+ reviewSteps,
+
+ //form feilds
+ passwordField,
+ usernameField,
+ passwordChange,
+ usernameChange,
+ emailField,
+ emailChange,
+
+ //methods
+ nextStep,
+ previousStep,
+ updateStep,
+ updateAutosaveForm,
+
+ //modal
+ isModalOpened,
+ isModalClosed,
+ };
+ },
+);
diff --git a/src/pages/acceptInvitation/AcceptInvitationReview.vue b/src/pages/acceptInvitation/AcceptInvitationReview.vue
new file mode 100644
index 000000000..270087bf7
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationReview.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+ {{ reviews.username }}
+
+
+
+
+ {{ error }}
+
+
+
+
+ {{ reviews.password }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{
+ field.name === 'email'
+ ? field.value
+ ? field.value
+ : t('common.noneProvided')
+ : ''
+ }}
+
+
+
+
+
+ {{
+ field.name === 'orcid'
+ ? field.value
+ ? field.value
+ : t('common.noneProvided')
+ : ''
+ }}
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{
+ field.name === 'givenName'
+ ? field.value
+ ? field.value
+ : t('common.noneProvided')
+ : ''
+ }}
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{
+ field.name === 'familyName'
+ ? field.value
+ ? field.value
+ : t('common.noneProvided')
+ : ''
+ }}
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{
+ field.name === 'affiliation'
+ ? field.value
+ ? field.value
+ : t('common.noneProvided')
+ : ''
+ }}
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ {{
+ field.name === 'country'
+ ? field.value
+ ? field.value
+ : t('common.noneProvided')
+ : ''
+ }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationUserRoles.vue b/src/pages/acceptInvitation/AcceptInvitationUserRoles.vue
new file mode 100644
index 000000000..75f0b1337
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationUserRoles.vue
@@ -0,0 +1,29 @@
+
+
+
+ Title
+ Start Date
+ End Date
+ Journal Masthead
+
+
+
+ {{ row.setting_value }}
+
+ {{ row.date_start }}
+ {{ row.date_end }}
+ testing
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue b/src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue
new file mode 100644
index 000000000..2a7cf97d3
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue
@@ -0,0 +1,11 @@
+
+ verify ORCID iD
+
+ Skip ORCID verification
+
+
+
diff --git a/src/pages/acceptInvitation/mocks/pageInitConfig.js b/src/pages/acceptInvitation/mocks/pageInitConfig.js
new file mode 100644
index 000000000..ab2b0f14e
--- /dev/null
+++ b/src/pages/acceptInvitation/mocks/pageInitConfig.js
@@ -0,0 +1,204 @@
+export default {
+ acceptInvitationApiUrl: 'https://mock/index.php/publicknowledge/api/v1/users',
+ primaryLocale: 'en',
+ pageTitle: 'Invite user to take a role',
+ pageTitleDescription:
+ 'You are inviting a user to take a role in OJS along with appearing in the journal masthead',
+ csrfToken: '3e964b3403ec2daf5d595121b888ab4c',
+ invitationId: 65,
+ invitationKey: '8aqc3W',
+ steps: [
+ {
+ id: 'verifyOrcid',
+ name: 'Verify ORCID iD',
+ reviewName: '',
+ stepName: '{$step} - Verify ORCID iD',
+ type: 'popup',
+ description: 'Please verify orcid iD',
+ sections: [
+ {
+ id: 'userVerifyOrcid',
+ sectionComponent: 'AcceptInvitationVerifyOrcid',
+ },
+ ],
+ },
+ {
+ id: 'userCreate',
+ name: 'Create OJS account',
+ reviewName: 'Account details',
+ stepName: '{$step} - Create OJS account',
+ type: 'form',
+ description:
+ 'To get started with OJS and accept the new role, you will need to create an account with us. For this purpose please enter a username and password.',
+ sections: [
+ {
+ id: 'userCreateForm',
+ sectionComponent: 'AcceptInvitationCreateUserAccount',
+ },
+ ],
+ reviewData: [],
+ },
+ {
+ id: 'userDetails',
+ name: 'Enter details',
+ reviewName: 'User Details',
+ stepName: '{$step} - Enter details',
+ type: 'form',
+ description:
+ 'Enter your details like email ID, affiliation ect. As per the GDPR compliance, this information can only modified by you. You can also choose if you want this information to be visible on your profile to the editor. ',
+ sections: [
+ {
+ id: 'userCreateDetailsForm',
+ type: 'form',
+ description:
+ 'Please provide the following details to help us manage your submission in our system.
',
+ form: {
+ id: 'userDetails',
+ method: 'POST',
+ action:
+ 'http://localhost/ojs/index.php/publicknowledge/api/v1/users',
+ fields: [
+ {
+ name: 'email',
+ component: 'field-text',
+ label: 'Email address',
+ groupId: 'default',
+ isRequired: true,
+ isMultilingual: false,
+ value: 'test@mailinator.com',
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'orcid',
+ component: 'field-text',
+ label: 'ORCID iD',
+ groupId: 'default',
+ isRequired: false,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'givenName',
+ component: 'field-text',
+ label: 'Given Name',
+ groupId: 'default',
+ isRequired: true,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'familyName',
+ component: 'field-text',
+ label: 'Family Name',
+ groupId: 'default',
+ isRequired: true,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'affiliation',
+ component: 'field-text',
+ label: 'Affiliation',
+ groupId: 'default',
+ isRequired: true,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'country',
+ component: 'field-text',
+ label: 'County of affiliation',
+ groupId: 'default',
+ isRequired: true,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ ],
+ groups: [
+ {
+ id: 'default',
+ pageId: 'default',
+ },
+ ],
+ hiddenFields: {},
+ pages: [
+ {
+ id: 'default',
+ submitButton: {
+ label: 'Save',
+ },
+ },
+ ],
+ primaryLocale: 'en',
+ visibleLocales: ['en'],
+ supportedFormLocales: [
+ {
+ key: 'en',
+ label: 'English',
+ },
+ {
+ key: 'fr_CA',
+ label: 'French',
+ },
+ ],
+ errors: {},
+ },
+ sectionComponent: 'AcceptInvitationCreateUserForms',
+ },
+ ],
+ },
+ {
+ id: 'userCreateReview',
+ name: 'Review & create account',
+ reviewName: 'Roles',
+ stepName: '{$step} - Review & create account',
+ type: 'review',
+ description: 'Review details to start your new roles in OJS',
+ sections: [
+ {
+ id: 'userCreateRoles',
+ sectionComponent: 'AcceptInvitationReview',
+ type: 'table',
+ description: '',
+ rows: [
+ {
+ date_start: '2024-03-01',
+ date_end: '2025-01-01',
+ user_group_id: 3,
+ setting_value: 'test',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
diff --git a/src/pages/userInvitation/UserInvitationDetailsFormStep.vue b/src/pages/userInvitation/UserInvitationDetailsFormStep.vue
new file mode 100644
index 000000000..6d26d2808
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationDetailsFormStep.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/src/pages/userInvitation/UserInvitationEmailComposerStep.vue b/src/pages/userInvitation/UserInvitationEmailComposerStep.vue
new file mode 100644
index 000000000..f9695ff2c
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationEmailComposerStep.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
diff --git a/src/pages/userInvitation/UserInvitationHeader.vue b/src/pages/userInvitation/UserInvitationHeader.vue
new file mode 100644
index 000000000..f67629358
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationHeader.vue
@@ -0,0 +1,17 @@
+
+
+ {{ pageTitle }}
+
+
+ {{ pageTitleDescription }}
+
+
+
+
diff --git a/src/pages/userInvitation/UserInvitationPage.mdx b/src/pages/userInvitation/UserInvitationPage.mdx
new file mode 100644
index 000000000..67e6c1131
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationPage.mdx
@@ -0,0 +1,9 @@
+import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks';
+
+import * as UserInvitationPage from './UserInvitationPage.stories.js';
+
+
+
+# User Invitation page
+
+
diff --git a/src/pages/userInvitation/UserInvitationPage.stories.js b/src/pages/userInvitation/UserInvitationPage.stories.js
new file mode 100644
index 000000000..b73b2e4f1
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationPage.stories.js
@@ -0,0 +1,32 @@
+import UserInvitationPage from './UserInvitationPage.vue';
+import {http, HttpResponse} from 'msw';
+import userMock from './mocks/userMock.js';
+import PageInitConfigMock from './mocks/pageInitConfig';
+
+export default {title: 'Pages/UserInvitation', component: UserInvitationPage};
+
+export const Init = {
+ render: (args) => ({
+ components: {UserInvitationPage},
+ setup() {
+ return {args};
+ },
+ template: '',
+ }),
+ parameters: {
+ msw: {
+ handlers: [
+ http.get('https://mock/index.php/publicknowledge/api/v1/_user', () => {
+ return HttpResponse.json(userMock);
+ }),
+ http.post(
+ 'https://mock/index.php/publicknowledge/api/v1/user/_invite',
+ () => {
+ return HttpResponse.json('invitation send successfully');
+ },
+ ),
+ ],
+ },
+ },
+ args: PageInitConfigMock,
+};
diff --git a/src/pages/userInvitation/UserInvitationPage.vue b/src/pages/userInvitation/UserInvitationPage.vue
new file mode 100644
index 000000000..cf457c718
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationPage.vue
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('invitation.wizard.errors') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('invitation.wizard.success', {email: store.email}) }}
+
+
+ {{ t('invitation.notification.closeBtn') }}
+
+
+
+
+
+
+
+
diff --git a/src/pages/userInvitation/UserInvitationPageStore.js b/src/pages/userInvitation/UserInvitationPageStore.js
new file mode 100644
index 000000000..1813fefb1
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationPageStore.js
@@ -0,0 +1,479 @@
+import {useTranslation} from '@/composables/useTranslation';
+
+import {defineComponentStore} from '@/utils/defineComponentStore';
+import {useFetch} from '@/composables/useFetch';
+import {computed, onMounted, ref, watch} from 'vue';
+//let pageInitConfig = null;
+
+/*export function initSubmissionsPageStore(_pageInitConfig) {
+ pageInitConfig = _pageInitConfig;
+}
+
+export function disposeSubmissionsPageStore() {
+ const store = useSubmissionsPageStore();
+ store.$dispose();
+ pageInitConfig = null;
+ delete getActivePinia().state.value[store.$id];
+}*/
+
+export const useUserInvitationPageStore = defineComponentStore(
+ 'userInvitationPage',
+ (pageInitConfig) => {
+ /**
+ * Translation
+ */
+
+ const {t} = useTranslation();
+
+ const currentStepId = ref(pageInitConfig.steps[0].id);
+ const steps = ref(pageInitConfig.steps);
+ const pageTitleDescription = ref(pageInitConfig.pageTitleDescription);
+ const errors = ref({});
+ const startedSteps = ref([]);
+ const userId = ref(null);
+ const isModalOpened = ref(false);
+
+ const emailField = ref({
+ label: t('user.email'),
+ name: 'email',
+ size: 'large',
+ value: '',
+ });
+ const usernameField = ref({
+ label: t('user.username'),
+ name: 'username',
+ size: 'large',
+ value: '',
+ });
+ const orcidField = ref({
+ label: t('user.orcid'),
+ name: 'orcid',
+ size: 'large',
+ value: '',
+ });
+ const email = ref('');
+ const username = ref('');
+ const orcid = ref('');
+ const recipientOptions = ref([]);
+ /**
+ * The currently active step
+ */
+ const currentStep = computed(() => {
+ return steps.value.find((step) => step.id === currentStepId.value);
+ });
+
+ /**
+ * The index of the currently active step
+ * in the steps array
+ */
+ const currentStepIndex = computed(() => {
+ return steps.value.findIndex((step) => step.id === currentStepId.value);
+ });
+
+ /**
+ * Is the current step the first step?
+ */
+ const isOnFirstStep = computed(() => {
+ return !currentStepIndex.value;
+ });
+
+ /**
+ * Is the current step the last step?
+ */
+ const isOnLastStep = computed(() => {
+ return currentStepIndex.value === steps.value.length - 1;
+ });
+
+ /**
+ * Are there any validation errors?
+ */
+ const isValid = computed(() => {
+ return Object.keys(errors.value).length === 0;
+ });
+ /**
+ * The title to show at the top of the page
+ */
+ const pageTitle = computed(() => {
+ if (!currentStep.value) {
+ return '';
+ }
+ return currentStep.value.name.replace('{$step}', currentStep.value);
+ });
+
+ /**
+ * The step title to show at the top of the step
+ */
+ const stepTitle = computed(() => {
+ if (!currentStep.value) {
+ return '';
+ }
+ return currentStep.value.reviewName.replace(
+ '{$step}',
+ 'STEP -' + (1 + currentStepIndex.value),
+ );
+ });
+
+ /**
+ * create searchPhrase
+ */
+ const searchPhrase = computed(() => {
+ let seachText = '';
+ if (email.value) {
+ seachText = email.value + ' ';
+ }
+ if (orcid.value) {
+ seachText = seachText + orcid.value + ' ';
+ }
+ if (username.value) {
+ seachText = seachText + username.value;
+ }
+
+ return seachText;
+ });
+
+ /**
+ * Update when the step changes
+ */
+ watch(currentStepIndex, async (newVal, oldVal) => {
+ if (newVal === oldVal) {
+ return;
+ }
+
+ // Update the list of steps that have been started
+ steps.value.forEach((step, i) => {
+ if (
+ !startedSteps.value.includes(step.id) &&
+ i <= currentStepIndex.value
+ ) {
+ startedSteps.value.push(step.id);
+ }
+ });
+
+ // Track step changes in the title and browser history
+ const step = steps.value[newVal];
+ // document.title = this.getPageTitle(step);
+ if (step.id !== window.location.hash.replace('#', '')) {
+ addHistory(step);
+ }
+
+ // Trigger validation on the review step
+ if (newVal === steps.value.length - 1) {
+ // validate();
+ }
+ });
+
+ /**
+ * Set form data when validation errors are changed
+ */
+ watch(errors, async (newVal, oldVal) => {
+ const keys = Object.keys(newVal);
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type === 'form') {
+ section.form.fields.forEach((field) => {
+ if (keys.includes(field.name)) {
+ steps.value[stepIndex].sections[sectionIndex].form.errors = {
+ ...steps.value[stepIndex].sections[sectionIndex].form.errors,
+ ...{[field.name]: newVal[field.name]},
+ };
+ }
+ });
+ }
+ });
+ });
+ });
+
+ onMounted(() => {
+ /**
+ * Open the correct step when the page is loaded
+ */
+ if (!window.location.hash) {
+ openStep(steps.value[0].id);
+ }
+ });
+
+ function emailChange(fieldName, propName, newValue, localeKey) {
+ email.value = newValue;
+ }
+ function usernameChange(fieldName, propName, newValue, localeKey) {
+ username.value = newValue;
+ }
+ function orcidChange(fieldName, propName, newValue, localeKey) {
+ orcid.value = newValue;
+ }
+ /**
+ * Add a step change to the browser history so the
+ * user can use the browser's back button
+ *
+ * @param {Object} step The step to add
+ */
+ function addHistory(step) {
+ window.history.pushState({}, step.name, '#' + step.id);
+ }
+
+ /**
+ * Go to the next step or submit if this is the last step
+ */
+ function nextStep() {
+ if (isOnLastStep.value) {
+ submit();
+ } else if (isOnFirstStep.value) {
+ searchUser();
+ } else {
+ openStep(steps.value[1 + currentStepIndex.value].id);
+ updateEmailComposer();
+ }
+ }
+
+ /**
+ * Go to a step in the wizard
+ *
+ * @param {String} stepId
+ */
+ function openStep(stepId) {
+ const newStep = steps.value.find((step) => step.id === stepId);
+ if (!newStep) {
+ return;
+ }
+ if (stepId === 'userInvitedEmail') {
+ errors.value = {};
+ }
+ currentStepId.value = stepId;
+ }
+
+ /**
+ * Go to the previous step in the wizard
+ */
+ function previousStep() {
+ const previousIndex = currentStepIndex.value - 1;
+ if (previousIndex >= 0) {
+ openStep(steps.value[previousIndex].id);
+ }
+ }
+
+ /**
+ * Complete the submission
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ async function submit() {
+ const data = {
+ userId: userId.value,
+ actions: {},
+ };
+ steps.value.forEach((step) => {
+ step.sections.forEach((section) => {
+ if (section.type === 'form' || section.length > 0) {
+ section.form.fields.forEach((field) => {
+ if (field.value != null) {
+ data[field.name] = field.value;
+ }
+ });
+ } else if (section.type === 'email') {
+ data.actions['actionattachments'] = section.email.attachments;
+ data.actions['locale'] = section.email.locale;
+ data.actions['recipients'] = section.email.recipients;
+ data.actions['subject'] = section.email.subject;
+ data.actions['body'] = section.email.body;
+ }
+ });
+ });
+ const {
+ data: res,
+ validationError,
+ fetch,
+ } = useFetch(pageInitConfig.inviteUserApiUrl, {
+ expectValidationError: true,
+ method: 'POST',
+ body: data,
+ });
+ await fetch();
+ if (validationError.value) {
+ errors.value = validationError.value;
+ } else if (res.value && !validationError.value) {
+ isModalOpened.value = true;
+ }
+ }
+
+ /**
+ * Update a form with new data
+ *
+ * This is fired every time a form field changes, so
+ * resource-intensive code should not be run every
+ * time this method is called.
+ *
+ * @param {String} formId
+ * @param {Object} data
+ */
+ function updateForm(formId, data) {
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type !== 'form' || section.form.id !== formId) {
+ return;
+ }
+ steps.value[stepIndex].sections[sectionIndex].form = {
+ ...steps.value[stepIndex].sections[sectionIndex].form,
+ ...data,
+ };
+ steps.value[stepIndex].sections[sectionIndex].form.fields.forEach(
+ (field) => {
+ if (data[field.name] instanceof Object) {
+ field.value = data[field.name][pageInitConfig.primaryLocale];
+ } else {
+ field.value = data[field.name];
+ }
+ },
+ );
+ });
+ });
+ }
+
+ /**
+ * Update a form with new data
+ *
+ * This is fired every time a form field changes, so
+ * resource-intensive code should not be run every
+ * time this method is called.
+ */
+ function updateEmailComposer() {
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type !== 'email') {
+ return;
+ }
+ steps.value[stepIndex].sections[sectionIndex].email.variables[
+ pageInitConfig.primaryLocale
+ ].push({
+ key: 'recipientName',
+ value:
+ recipientOptions.value[0].label[pageInitConfig.primaryLocale],
+ });
+ steps.value[stepIndex].sections[sectionIndex].email = {
+ ...steps.value[stepIndex].sections[sectionIndex].email,
+ recipients: recipientOptions.value.map((to) => to.value),
+ recipientOptions: recipientOptions.value,
+ };
+ });
+ });
+ }
+
+ /**
+ * Complete the submission
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ async function searchUser() {
+ if (searchPhrase.value !== '') {
+ const {data: user, fetch} = useFetch(pageInitConfig.searchUserApiUrl, {
+ query: {searchPhrase: searchPhrase.value, status: 'all'},
+ });
+ await fetch();
+ let userObj = {};
+ if (user.value.items.length > 0) {
+ user.value.items.forEach((data) => {
+ userId.value = data.id;
+ email.value = data.email;
+ userObj = {
+ email: data.email,
+ givenName: data.fullName.split(' ')[0],
+ familyName: data.fullName.split(' ')[1],
+ orcid: data.orcid,
+ };
+ recipientOptions.value.push({
+ value: data.id,
+ label: {
+ [pageInitConfig.primaryLocale]: data.fullName,
+ },
+ });
+ });
+ } else {
+ errors.value = {
+ error: t('invitation.noUserFound'),
+ };
+ recipientOptions.value.push({
+ value: email.value,
+ label: {
+ [pageInitConfig.primaryLocale]: email.value,
+ },
+ });
+ userObj = {
+ email: email.value,
+ };
+ }
+ updateForm('userDetails', userObj);
+ openStep(steps.value[1 + currentStepIndex.value].id);
+ } else {
+ errors.value = {
+ error: t('invitation.emptySearchFields'),
+ };
+ }
+ }
+
+ /**
+ * Update the data attached to a step
+ *
+ * @param {String} stepId The id of the step to update
+ * @param {Object} data The data to update in the step
+ */
+ function updateStep(stepId, data) {
+ steps.value.forEach((step, stepIndex) => {
+ step.sections.forEach((section, sectionIndex) => {
+ if (section.type !== 'email') {
+ return;
+ }
+ let errors = {...step.sections.errors};
+ Object.keys(data).forEach((key) => delete errors[key]);
+ steps.value[stepIndex].sections[sectionIndex].email = {
+ ...steps.value[stepIndex].sections[sectionIndex].email,
+ ...data,
+ errors: errors,
+ };
+ });
+ });
+ }
+
+ /**
+ * Close modal popup
+ */
+ function isModalClosed() {
+ isModalOpened.value = false;
+ window.location = pageInitConfig.userInvitationSavedUrl;
+ }
+
+ return {
+ //computed
+ currentStep,
+ currentStepIndex,
+ isOnFirstStep,
+ isOnLastStep,
+ isValid,
+ pageTitle,
+ startedSteps,
+ stepTitle,
+ openStep,
+ steps,
+ pageTitleDescription,
+ errors,
+ email,
+
+ //form feilds
+ emailField,
+ usernameField,
+ orcidField,
+ emailChange,
+ orcidChange,
+ usernameChange,
+
+ //methods
+ nextStep,
+ previousStep,
+ updateStep,
+
+ //modal
+ isModalOpened,
+ isModalClosed,
+ };
+ },
+);
diff --git a/src/pages/userInvitation/UserInvitationSearchFormStep.vue b/src/pages/userInvitation/UserInvitationSearchFormStep.vue
new file mode 100644
index 000000000..a129d943d
--- /dev/null
+++ b/src/pages/userInvitation/UserInvitationSearchFormStep.vue
@@ -0,0 +1,27 @@
+
+
+
+ or
+
+
+
+ or
+
+
+
+
+
diff --git a/src/pages/userInvitation/mocks/pageInitConfig.js b/src/pages/userInvitation/mocks/pageInitConfig.js
new file mode 100644
index 000000000..e11638a0d
--- /dev/null
+++ b/src/pages/userInvitation/mocks/pageInitConfig.js
@@ -0,0 +1,223 @@
+import InsertContentMock from '@/mocks/insertContent';
+
+export default {
+ searchUserApiUrl: 'https://mock/index.php/publicknowledge/api/v1/_user',
+ emailTemplatesApiUrl:
+ 'https://mock/index.php/publicknowledge/api/v1/EmailTemplateMocks',
+ inviteUserApiUrl:
+ 'https://mock/index.php/publicknowledge/api/v1/user/_invite',
+ userInvitationSavedUrl: window.location.href,
+ pageTitle: 'Invite user to take a role',
+ pageTitleDescription:
+ 'You are inviting a user to take a role in OJS along with appearing in the journal masthead',
+ primaryLocale: 'en',
+ steps: [
+ {
+ id: 'searchUser',
+ name: 'Search User',
+ reviewName: '{$step} - Search User',
+ type: 'form',
+ description:
+ 'Search for the user using their email address, username or ORCID ID. Enter at least one details to get started. If user does not exist, ypu can invite them to take up roles and be a part of your journal. If the user already exist in the system, you can view user information and invite to take a additional roles.',
+ sections: [
+ {
+ id: 'searchUserForm',
+ sectionComponent: 'UserInvitationSearchFormStep',
+ },
+ ],
+ reviewTemplate: '/management/invitation/userSearch.tpl',
+ },
+ {
+ id: 'userDetails',
+ name: 'Enter details',
+ reviewName: '{$step} - Enter details and invite for roles',
+ type: 'form',
+ description: 'You can invite them to take up a role in OJS',
+ sections: [
+ {
+ id: 'userDetailsForm',
+ type: 'form',
+ description:
+ 'Please provide the following details to help us manage your submission in our system.
',
+ sectionComponent: 'UserInvitationDetailsFormStep',
+ form: {
+ id: 'userDetails',
+ method: 'POST',
+ action:
+ 'http://localhost/ojs/index.php/publicknowledge/api/v1/users',
+ fields: [
+ {
+ name: 'email',
+ component: 'field-text',
+ label: 'Email address',
+ groupId: 'default',
+ isRequired: true,
+ isMultilingual: false,
+ value: 'null',
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'orcid',
+ component: 'field-text',
+ label: 'ORCID iD',
+ groupId: 'default',
+ isRequired: false,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'givenName',
+ component: 'field-text',
+ label: 'Given Name',
+ groupId: 'default',
+ isRequired: false,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ {
+ name: 'familyName',
+ component: 'field-text',
+ label: 'Family Name',
+ groupId: 'default',
+ isRequired: false,
+ isMultilingual: false,
+ value: null,
+ inputType: 'text',
+ optIntoEdit: false,
+ optIntoEditLabel: '',
+ size: 'large',
+ prefix: '',
+ },
+ ],
+ groups: [
+ {
+ id: 'default',
+ pageId: 'default',
+ },
+ ],
+ hiddenFields: {},
+ pages: [
+ {
+ id: 'default',
+ submitButton: {
+ label: 'Save',
+ },
+ },
+ ],
+ primaryLocale: 'en',
+ visibleLocales: ['en'],
+ supportedFormLocales: [
+ {
+ key: 'en',
+ label: 'English',
+ },
+ {
+ key: 'fr_CA',
+ label: 'French',
+ },
+ ],
+ errors: {},
+ },
+ },
+ ],
+ reviewTemplate: '/management/invitation/userDetails.tpl',
+ },
+ {
+ id: 'userInvitedEmail',
+ name: 'Review & invite for roles',
+ reviewName: '{$step} - Modify email shared with the user',
+ type: 'email',
+ description:
+ 'Send the user an email to let them know about the invitation, next steps, journal GDPR polices and ORCiD verification',
+ sections: [
+ {
+ id: 'userInvited',
+ type: 'email',
+ description:
+ 'Please provide the following details to help us manage your submission in our system.
',
+ sectionComponent: 'UserInvitationEmailComposerStep',
+ email: {
+ id: 'userInvited',
+ type: 'email',
+ name: 'Invite Users',
+ attachers: [],
+ canChangeRecipients: false,
+ canSkip: true,
+ description:
+ 'Send an email to the authors to let them know that this submission will be sent for peer review. If possible, give the authors some indication of how long the peer review process might take and when they should expect to hear from the editors again. This email will not be sent until the decision is recorded.',
+ errors: {},
+ initialTemplateKey: 'EDITOR_DECISION_ACCEPT',
+ anonymousRecipients: false,
+ locale: 'en',
+ body: "{$recipientName}
\\n\n
\\n\nYou have now been registered as a user with Journal of Public Knowledge. We have included your username and password in this email, which are needed for all work with this journal through its website. At any point, you can ask to be removed from the journal's list of users by contacting me.
\\n\n
\\n\naccept url: {$acceptUrl}
\\n\ndecline url: {$declineUrl}
\\n\n
\\n\nThank you,
\\n\nadmin admin
\n",
+ subject: 'User invited',
+ emailTemplates: [
+ {
+ _href:
+ 'http://localhost:8000/pu/api/v1/emailTemplates/DISCUSSION_NOTIFICATION_COPYEDITING',
+ alternateTo: null,
+ body: {
+ fr_CA: 'Prière de saisir votre message.',
+ en: 'Please enter your message.',
+ },
+ contextId: null,
+ id: null,
+ key: 'DISCUSSION_NOTIFICATION_COPYEDITING',
+ name: {
+ fr_CA: '',
+ en: 'Discussion (Copyediting)',
+ },
+ subject: {
+ fr_CA: 'Un message à propos de la revue {$journalName}',
+ en: 'A message regarding {$journalName}',
+ },
+ },
+ {
+ _href:
+ 'http://localhost:8000/pu/api/v1/emailTemplates/COPYEDIT_REQUEST',
+ alternateTo: 'DISCUSSION_NOTIFICATION_COPYEDITING',
+ body: {
+ en: 'Dear {$recipientName},
A new submission is ready to be copyedited:
{$submissionId} {$submissionTitle}
{$journalName}
Please follow these steps to complete this task:
- 1. Click on the Submission URL below.
- 2. Open any files available under Draft Files and edit the files. Use the Copyediting Discussions area if you need to contact the editor(s) or author(s).
- 3. Save the copyedited file(s) and upload them to the Copyedited panel.
- 4. Use the Copyediting Discussions to notify the editor(s) that all files have been prepared, and that the Production process may begin.
If you are unable to undertake this work at this time or have any questions, please contact me. Thank you for your contribution to this journal.
Kind regards,
{$signature}',
+ fr_CA:
+ "{$recipientName},
\n
\nJ'aimerais que vous effectuiez la révision du manuscrit intitulé « {$submissionTitle} » pour la revue {$journalName} à l'aide des étapes suivantes.
\n1. Cliquer sur l'URL de la soumission ci-dessous.
\n2. Ouvrir le(s) fichier(s) disponible(s) sous Fichiers des ébauches finales et effectuer votre révision, tout en ajoutant des discussions sur la révision, le cas échéant.
\n3. Enregistrer le(s) fichier(s) révisé(s) et le(s) téléverser dans la section Version(s) révisée(s).
\n4. Informer le,la rédacteur-trice que tous les fichiers ont été révisés et que l'étape de production peut débuter.
\n
\nURL de la revue {$journalName} : {$journalUrl}
\nURL de la soumission : {$submissionUrl}
\nNom d'utilisateur-trice : {$recipientUsername}",
+ },
+ contextId: 1,
+ id: 1,
+ key: 'COPYEDIT_REQUEST',
+ name: {
+ en: 'Request Copyedit',
+ fr_CA: '',
+ },
+ subject: {
+ en: 'Submission {$submissionId} is ready to be copyedited for {$contextAcronym}',
+ fr_CA: "Demande de révision d'une soumission",
+ },
+ },
+ ],
+ defaultEmailTemplateMockKey: 'EDITOR_DECISION_ACCEPT',
+ EmailTemplateMocksApiUrl: 'http://example.org',
+ variables: {
+ en: [...InsertContentMock],
+ fr_CA: [...InsertContentMock],
+ },
+ },
+ },
+ ],
+ reviewTemplate: '/management/invitation/userInvitation.tpl',
+ },
+ ],
+};
diff --git a/src/pages/userInvitation/mocks/userMock.js b/src/pages/userInvitation/mocks/userMock.js
new file mode 100644
index 000000000..27e6fbd34
--- /dev/null
+++ b/src/pages/userInvitation/mocks/userMock.js
@@ -0,0 +1,67 @@
+export default {
+ itemsMax: 1,
+ items: [
+ {
+ _href: 'http://localhost/ojs/index.php/publicknowledge/api/v1/users/35',
+ affiliation: {
+ en: 'CUNY',
+ fr_CA: '',
+ },
+ biography: {
+ en: '',
+ fr_CA: '',
+ },
+ disabled: false,
+ email: 'carlo@mailinator.com',
+ fullName: 'Carlo Corino',
+ groups: [
+ {
+ id: 17,
+ name: {
+ en: 'Reader',
+ fr_CA: 'Lecteur-trice',
+ },
+ abbrev: {
+ en: 'Read',
+ fr_CA: 'Lect',
+ },
+ roleId: 1048576,
+ showTitle: true,
+ permitSelfRegistration: true,
+ permitMetadataEdit: false,
+ recommendOnly: false,
+ },
+ {
+ id: 14,
+ name: {
+ en: 'Author',
+ fr_CA: 'Auteur-e',
+ },
+ abbrev: {
+ en: 'AU',
+ fr_CA: 'AU',
+ },
+ roleId: 65536,
+ showTitle: true,
+ permitSelfRegistration: true,
+ permitMetadataEdit: false,
+ recommendOnly: false,
+ },
+ ],
+ id: 35,
+ interests: [],
+ orcid: null,
+ orcidAccessDenied: null,
+ orcidAccessExpiresOn: null,
+ orcidAccessScope: null,
+ orcidAccessToken: null,
+ orcidRefreshToken: null,
+ orcidReviewPutCode: null,
+ preferredPublicName: {
+ en: '',
+ fr_CA: '',
+ },
+ userName: 'zwoods',
+ },
+ ],
+};