diff --git a/public/globals.js b/public/globals.js
index 388dafe3d..481c59c74 100644
--- a/public/globals.js
+++ b/public/globals.js
@@ -391,7 +391,8 @@ window.pkp = {
'invitation.role.masthead' : 'Journal Masthead',
'invitation.role.removeRole.button' : 'Remove Role',
'invitation.role.addRole.button':'Add Another Role',
- 'invitation.orcid.message':'Add Another Role',
+ 'invitation.orcid.message':'On accepting the invite, the user will redirected to ORCID to verify their account, if the wish to',
+ 'invitation.orcid.acceptInvitation.message':'Not verified. You can verify your ORCID iD from your profile section in OJS',
},
diff --git a/src/components/Container/PageOJS.vue b/src/components/Container/PageOJS.vue
index dd798b54e..f13455ad5 100644
--- a/src/components/Container/PageOJS.vue
+++ b/src/components/Container/PageOJS.vue
@@ -2,11 +2,13 @@
import Page from '@/components/Container/Page.vue';
import SubmissionsPage from '@/pages/submissions/SubmissionsPage.vue';
import UserInvitationPage from '@/pages/userInvitation/UserInvitationPage.vue';
+import AcceptInvitationPage from '@/pages/acceptInvitation/AcceptInvitationPage.vue';
export default {
components: {
SubmissionsPage,
UserInvitationPage,
+ AcceptInvitationPage,
},
extends: Page,
};
diff --git a/src/composables/useForm.js b/src/composables/useForm.js
index 8aba64950..d807e864f 100644
--- a/src/composables/useForm.js
+++ b/src/composables/useForm.js
@@ -53,6 +53,7 @@ export function useForm(_form) {
watch(
errors,
(newErrors) => {
+ form.value.errors = {};
Object.keys(newErrors).forEach((key) => {
if (doesFieldExist(form.value, key)) {
form.value.errors[key] = newErrors[key];
diff --git a/src/pages/acceptInvitation/AcceptInvitationCreateUserAccount.vue b/src/pages/acceptInvitation/AcceptInvitationCreateUserAccount.vue
new file mode 100644
index 000000000..6460eef70
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationCreateUserAccount.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+ {{ store.invitationPayload.email }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue b/src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue
new file mode 100644
index 000000000..1d759abf8
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationCreateUserForms.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+ {{ store.invitationPayload.email }}
+
+
+
+
+
+
+
+ {{
+ store.invitationPayload.orcid
+ ? store.invitationPayload.orcid
+ : t('invitation.orcid.acceptInvitation.message')
+ }}
+
+
+
+
+
+
+
+
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..2d440d1d4
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPage.stories.js
@@ -0,0 +1,44 @@
+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');
+ }),
+ http.post(
+ 'https://mock/index.php/publicknowledge/api/v1/invitations/65/key/8aqc3W',
+ async ({request}) => {
+ const postBody = await request.json();
+ let errors = {};
+ Object.keys(postBody).forEach((element) => {
+ if (postBody[element] === '') {
+ errors[element] = ['This field is required'];
+ }
+ });
+ if (Object.keys(errors).length > 0) {
+ return HttpResponse.json(errors, {status: 422});
+ }
+ return HttpResponse.json(postBody, {status: 200});
+ },
+ ),
+ ],
+ },
+ },
+ args: PageInitConfigMock,
+};
diff --git a/src/pages/acceptInvitation/AcceptInvitationPage.vue b/src/pages/acceptInvitation/AcceptInvitationPage.vue
new file mode 100644
index 000000000..fc917d2ee
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPage.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('invitation.wizard.errors') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationPageStore.js b/src/pages/acceptInvitation/AcceptInvitationPageStore.js
new file mode 100644
index 000000000..cc2a7b7a8
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationPageStore.js
@@ -0,0 +1,302 @@
+import {defineComponentStore} from '@/utils/defineComponentStore';
+import {useFetch} from '@/composables/useFetch';
+import {computed, onMounted, ref, watch} from 'vue';
+import {useUrl} from '@/composables/useUrl';
+import {useModal} from '@/composables/useModal';
+
+export const useAcceptInvitationPageStore = defineComponentStore(
+ 'userInvitationPage',
+ (pageInitConfig) => {
+ const {openDialog} = useModal();
+
+ /** Invitation payload, initial value*/
+ const invitationPayload = ref({...pageInitConfig.invitationPayload});
+
+ function updatePayload(fieldName, value) {
+ invitationPayload.value[fieldName] = value;
+ }
+
+ /** Steps */
+ const currentStepId = ref(pageInitConfig.steps[0].id);
+ const steps = ref(pageInitConfig.steps);
+ const startedSteps = 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;
+ });
+
+ /**
+ * 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
+ */
+ async function nextStep() {
+ if (isOnLastStep.value) {
+ submit();
+ } else {
+ await updateInvitationPayload();
+ if (currentStepErrorsPerSection.value.length === 0) {
+ 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;
+ }
+ 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);
+ }
+ }
+
+ /** Errors */
+ const errors = ref({});
+ /**
+ * Set current step section errors
+ */
+ const currentStepErrorsPerSection = computed(() => {
+ let error = [];
+ if (Object.keys(errors.value).length != 0) {
+ currentStep.value.sections.forEach((element, index) => {
+ if (element.props.validateFields.length > 0) {
+ let sectionErr = {};
+ element.props.validateFields.forEach((field) => {
+ if (Object.keys(errors.value).includes(field)) {
+ sectionErr[field] = errors.value[field];
+ }
+ });
+ if (Object.keys(sectionErr).length != 0) {
+ error[index] = sectionErr;
+ }
+ }
+ });
+ }
+
+ return error;
+ });
+
+ /**
+ * Are there any validation errors?
+ */
+ const isValid = computed(() => {
+ return Object.keys(errors.value).length === 0;
+ });
+
+ /** Page titles*/
+ const pageTitleDescription = ref(pageInitConfig.pageTitleDescription);
+ /**
+ * 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),
+ );
+ });
+
+ /**
+ * The step next button name
+ */
+ const stepButtonTitle = computed(() => {
+ if (!currentStep.value) {
+ return '';
+ }
+ return currentStep.value.stepButtonName;
+ });
+
+ /**
+ * 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];
+ if (step.id !== window.location.hash.replace('#', '')) {
+ addHistory(step);
+ }
+ });
+
+ /** Create update invitation url */
+ const updateInvitationUrl = computed(() => {
+ const {apiUrl} = useUrl('invitations');
+ return (
+ apiUrl.value +
+ '/' +
+ pageInitConfig.invitationId +
+ '/key/' +
+ pageInitConfig.invitationKey
+ );
+ });
+
+ /**
+ * Update the payload
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ async function updateInvitationPayload() {
+ const {validationError, fetch} = useFetch(updateInvitationUrl, {
+ expectValidationError: true,
+ method: 'PUT',
+ body: invitationPayload.value,
+ });
+ await fetch();
+ if (validationError.value) {
+ errors.value = validationError.value;
+ } else {
+ errors.value = [];
+ }
+ }
+
+ /** Create accpet invitation url */
+ const finalizeInvitationUrl = computed(() => {
+ const {apiUrl} = useUrl('invitations');
+ return (
+ apiUrl.value +
+ '/' +
+ pageInitConfig.invitationId +
+ '/key/' +
+ pageInitConfig.invitationKey
+ );
+ });
+ /**
+ * Complete the submission
+ *
+ * Opens a confirmation dialog and then makes the submission
+ * request with any required confirmation fields
+ */
+ async function submit() {
+ await updateInvitationPayload();
+ if (isValid.value) {
+ const {data, fetch} = useFetch(finalizeInvitationUrl, {
+ expectValidationError: true,
+ method: 'PUT',
+ body: invitationPayload.value,
+ });
+
+ await fetch();
+ if (data.value) {
+ openDialog({
+ title: 'Invitation sent',
+ actions: [
+ {
+ label: 'Ok',
+ callback: (close) => {
+ close();
+ },
+ },
+ ],
+ });
+ }
+ }
+ }
+
+ onMounted(() => {
+ /**
+ * Open the correct step when the page is loaded
+ */
+ if (!window.location.hash) {
+ openStep(steps.value[0].id);
+ }
+ });
+
+ return {
+ //computed
+ currentStep,
+ currentStepIndex,
+ isOnFirstStep,
+ isOnLastStep,
+ isValid,
+ pageTitle,
+ startedSteps,
+ stepTitle,
+ openStep,
+ steps,
+ pageTitleDescription,
+ errors,
+ stepButtonTitle,
+
+ //methods
+ nextStep,
+ previousStep,
+ updatePayload,
+
+ //refs
+ invitationPayload,
+ };
+ },
+);
diff --git a/src/pages/acceptInvitation/AcceptInvitationReview.vue b/src/pages/acceptInvitation/AcceptInvitationReview.vue
new file mode 100644
index 000000000..cddbaea13
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationReview.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+ {{ store.invitationPayload.username }}
+
+
+
+ {{ store.invitationPayload.password }}
+
+
+
+
+
+
+
+
+ {{ store.invitationPayload.email }}
+
+
+
+ {{
+ store.invitationPayload.orcid
+ ? store.invitationPayload.orcid
+ : t('invitation.orcid.acceptInvitation.message')
+ }}
+
+
+
+
+ {{ store.invitationPayload.givenName }}
+
+
+
+ {{ store.invitationPayload.familyName }}
+
+
+
+ {{ store.invitationPayload.affiliation }}
+
+
+
+ {{ store.invitationPayload.country }}
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationUserRoles.vue b/src/pages/acceptInvitation/AcceptInvitationUserRoles.vue
new file mode 100644
index 000000000..f58323a2c
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationUserRoles.vue
@@ -0,0 +1,34 @@
+
+
+
+ Title
+ Start Date
+ End Date
+ Journal Masthead
+
+
+
+
+ {{ row.setting_value }}
+
+ {{ row.date_start }}
+ {{ row.date_end }}
+ tesing
+
+
+
+
+
+
diff --git a/src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue b/src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue
new file mode 100644
index 000000000..19f15d696
--- /dev/null
+++ b/src/pages/acceptInvitation/AcceptInvitationVerifyOrcid.vue
@@ -0,0 +1,24 @@
+
+ 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..2c3552955
--- /dev/null
+++ b/src/pages/acceptInvitation/mocks/pageInitConfig.js
@@ -0,0 +1,203 @@
+export default {
+ 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',
+ invitationId: 65,
+ invitationKey: '8aqc3W',
+ invitationPayload: {
+ userId: null,
+ email: 'test@mailinator.com',
+ orcid: '',
+ username: '',
+ password: '',
+ givenName: 'test',
+ familyName: 'test',
+ affiliation: '',
+ country: '',
+ },
+ steps: [
+ {
+ id: 'verifyOrcid',
+ name: 'Verify ORCID iD',
+ reviewName: '',
+ stepName: '{$step} - Verify ORCID iD',
+ type: 'popup',
+ description: 'Please verify orcid iD',
+ stepButtonName: 'Save And Continue',
+ sections: [
+ {
+ id: 'userVerifyOrcid',
+ sectionComponent: 'AcceptInvitationVerifyOrcid',
+ props: {},
+ },
+ ],
+ },
+ {
+ 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.',
+ stepButtonName: 'Save And Continue',
+ sections: [
+ {
+ id: 'userCreateForm',
+ sectionComponent: 'AcceptInvitationCreateUserAccount',
+ props: {
+ validateFields: ['username', 'password'],
+ },
+ },
+ ],
+ 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. ',
+ stepButtonName: 'Accept And Continue to OJS',
+ sections: [
+ {
+ id: 'userCreateDetailsForm',
+ type: 'form',
+ description:
+ '
Please provide the following details to help us manage your submission in our system.
',
+ props: {
+ form: {
+ id: 'userDetails',
+ method: 'POST',
+ action:
+ 'http://localhost/ojs/index.php/publicknowledge/api/v1/users',
+ fields: [
+ {
+ 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: {},
+ },
+ validateFields: [
+ 'affiliation',
+ 'givenName',
+ 'familyName',
+ 'country',
+ ],
+ },
+ 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',
+ stepButtonName: 'Accept And Continue to OJS',
+ sections: [
+ {
+ id: 'userCreateRoles',
+ sectionComponent: 'AcceptInvitationReview',
+ type: 'table',
+ description: '',
+ props: {
+ rows: [
+ {
+ date_start: '2024-03-01',
+ date_end: '2025-01-01',
+ user_group_id: 3,
+ setting_value: 'test',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+};