diff --git a/classes/components/forms/invitation/AcceptUserDetailsForm.php b/classes/components/forms/invitation/AcceptUserDetailsForm.php new file mode 100644 index 00000000000..cc47b615019 --- /dev/null +++ b/classes/components/forms/invitation/AcceptUserDetailsForm.php @@ -0,0 +1,87 @@ +action = $action; + $this->locales = $locales; + + $countries = []; + foreach (Locale::getCountries() as $country) { + $countries[] = [ + 'value' => $country->getAlpha2(), + 'label' => $country->getLocalName() + ]; + } + + usort($countries, function ($a, $b) { + return strcmp($a['label'], $b['label']); + }); + + $this->addField(new FieldText('givenName', [ + 'label' => __('user.givenName'), + 'description' => __('acceptInvitation.userDetailsForm.givenName.description'), + 'isRequired' => true, + 'isMultilingual' => true, + 'size' => 'large', + 'value' => '' + ])) + ->addField(new FieldText('familyName', [ + 'label' => __('user.familyName'), + 'description' => __('acceptInvitation.userDetailsForm.familyName.description'), + 'isRequired' => true, + 'isMultilingual' => true, + 'size' => 'large', + 'value' => '' + ])) + ->addField(new FieldText('affiliation', [ + 'label' => __('user.affiliation'), + 'description' => __('acceptInvitation.userDetailsForm.affiliation.description'), + 'isMultilingual' => true, + 'isRequired' => true, + 'size' => 'large', + + ])) + ->addField(new FieldSelect('userCountry', [ + 'label' => __('acceptInvitation.userDetailsForm.countryOfAffiliation.label'), + 'description' => __('acceptInvitation.userDetailsForm.countryOfAffiliation.description'), + 'options' => $countries, + 'isRequired' => true, + 'size' => 'large', + ])); + + } +} diff --git a/classes/components/forms/invitation/UserDetailsForm.php b/classes/components/forms/invitation/UserDetailsForm.php new file mode 100644 index 00000000000..bc3cf45734d --- /dev/null +++ b/classes/components/forms/invitation/UserDetailsForm.php @@ -0,0 +1,69 @@ +action = $action; + $this->locales = $locales; + + $this->addField(new FieldText('email', [ + 'label' => __('user.email'), + 'description' => __('invitation.email.description'), + 'isRequired' => true, + 'size' => 'large', + ])) + ->addField(new FieldHTML('orcid', [ + 'label' => __('user.orcid'), + 'description' => __('invitation.orcid.description'), + 'isRequired' => false, + 'size' => 'large', + ])) + ->addField(new FieldText('givenName', [ + 'label' => __('user.givenName'), + 'description' => __('invitation.givenName.description'), + 'isRequired' => false, + 'isMultilingual' => true, + 'size' => 'large', + ])) + ->addField(new FieldText('familyName', [ + 'label' => __('user.familyName'), + 'description' => __('invitation.familyName.description'), + 'isRequired' => false, + 'isMultilingual' => true, + 'size' => 'large', + ])); + } +} diff --git a/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php b/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php index c0832538780..07b923a5868 100644 --- a/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php +++ b/classes/invitation/invitations/userRoleAssignment/handlers/UserRoleAssignmentInviteRedirectController.php @@ -15,9 +15,12 @@ use APP\core\Request; use APP\template\TemplateManager; +use PKP\core\PKPApplication; use PKP\invitation\core\enums\InvitationAction; +use PKP\invitation\core\enums\InvitationStatus; use PKP\invitation\core\InvitationActionRedirectController; use PKP\invitation\invitations\userRoleAssignment\UserRoleAssignmentInvite; +use PKP\invitation\stepTypes\AcceptInvitationStep; class UserRoleAssignmentInviteRedirectController extends InvitationActionRedirectController { @@ -29,14 +32,45 @@ public function getInvitation(): UserRoleAssignmentInvite public function acceptHandle(Request $request): void { $templateMgr = TemplateManager::getManager($request); - $templateMgr->assign('invitation', $this->invitation); - $templateMgr->display('frontend/pages/invitations.tpl'); + $context = $request->getContext(); + $steps = new AcceptInvitationStep(); + $templateMgr->setState([ + 'steps' => $steps->getSteps($this->invitation, $context), + 'primaryLocale' => $context->getData('primaryLocale'), + 'pageTitle' => __('invitation.wizard.pageTitle'), + 'invitationId' => (int)$request->getUserVar('id') ?: null, + 'invitationKey' => $request->getUserVar('key') ?: null, + 'pageTitleDescription' => __('invitation.wizard.pageTitleDescription'), + ]); + $templateMgr->assign([ + 'pageComponent' => 'PageOJS', + ]); + $templateMgr->display('invitation/acceptInvitation.tpl'); } public function declineHandle(Request $request): void { - return; + if ($this->invitation->getStatus() !== InvitationStatus::PENDING) { + $request->getDispatcher()->handle404(); + } + + $context = $request->getContext(); + + $url = PKPApplication::get()->getDispatcher()->url( + PKPApplication::get()->getRequest(), + PKPApplication::ROUTE_PAGE, + $context->getData('urlPath'), + 'login', + null, + null, + [ + ] + ); + + $this->getInvitation()->decline(); + + $request->redirectUrl($url); } public function preRedirectActions(InvitationAction $action) diff --git a/classes/invitation/sections/Email.php b/classes/invitation/sections/Email.php new file mode 100644 index 00000000000..8e80d42a2bf --- /dev/null +++ b/classes/invitation/sections/Email.php @@ -0,0 +1,98 @@ + $recipients One or more User objects who are the recipients of this email + * @param Mailable $mailable The mailable that will be used to send this email + * + * @throws Exception + */ + public function __construct(string $id, string $name, string $description, array $recipients, Mailable $mailable, array $locales) + { + parent::__construct($id, $name, $description); + $this->locales = $locales; + $this->mailable = $mailable; + $this->recipients = $recipients; + } + + public function getState(): stdClass + { + $config = parent::getState(); + $config->canChangeRecipients = false; + $config->canSkip = false; + $config->emailTemplates = $this->getEmailTemplates(); + $config->initialTemplateKey = $this->mailable::getEmailTemplateKey(); + $config->recipientOptions = $this->getRecipientOptions(); + $config->anonymousRecipients = $this->anonymousRecipients; + $config->variables = []; + $config->locale = Locale::getLocale(); + $config->locales = []; + return $config; + } + + protected function getRecipientOptions(): array + { + $recipientOptions = []; + foreach ($this->recipients as $user) { + $names = []; + foreach ($this->locales as $locale) { + $names[$locale] = $user->getFullName(true, false, $locale); + } + $recipientOptions[] = [ + 'value' => $user->getId(), + 'label' => $names, + ]; + } + return $recipientOptions; + } + + protected function getEmailTemplates(): array + { + $request = Application::get()->getRequest(); + $context = $request->getContext(); + + $emailTemplates = collect(); + if ($this->mailable::getEmailTemplateKey()) { + $emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $this->mailable::getEmailTemplateKey()); + if ($emailTemplate) { + $emailTemplates->add($emailTemplate); + } + Repo::emailTemplate() + ->getCollector($context->getId()) + ->alternateTo([$this->mailable::getEmailTemplateKey()]) + ->getMany() + ->each(fn (EmailTemplate $e) => $emailTemplates->add($e)); + } + + return Repo::emailTemplate()->getSchemaMap()->mapMany($emailTemplates)->toArray(); + } +} diff --git a/classes/invitation/sections/Form.php b/classes/invitation/sections/Form.php new file mode 100644 index 00000000000..c62ba7fb3db --- /dev/null +++ b/classes/invitation/sections/Form.php @@ -0,0 +1,45 @@ +form = $form; + } + + public function getState(): stdClass + { + $config = parent::getState(); + foreach ($this->form->getConfig() as $key => $value) { + $config->$key = $value; + } + unset($config->pages[0]['submitButton']); + + return $config; + } +} diff --git a/classes/invitation/sections/Section.php b/classes/invitation/sections/Section.php new file mode 100644 index 00000000000..89fe3e96d42 --- /dev/null +++ b/classes/invitation/sections/Section.php @@ -0,0 +1,54 @@ +id = $id; + $this->name = $name; + $this->description = $description; + if (!isset($this->type)) { + throw new Exception('Decision workflow step created without specifying a type.'); + } + } + + /** + * Compile initial state data to pass to the frontend + */ + public function getState(): stdClass + { + $config = new stdClass(); + $config->id = $this->id; + $config->type = $this->type; + $config->name = $this->name; + $config->description = $this->description; + $config->errors = new stdClass(); + + return $config; + } +} diff --git a/classes/invitation/sections/Sections.php b/classes/invitation/sections/Sections.php new file mode 100644 index 00000000000..abce3d0f69b --- /dev/null +++ b/classes/invitation/sections/Sections.php @@ -0,0 +1,75 @@ +id = $id; + $this->name = $name; + $this->description = $description; + $this->sectionComponent = $sectionComponent; + $this->type = $type; + } + /** + * Add a step to the invitation + */ + public function addSection($section, $props): void + { + if(is_null($section)) { + $this->sections[] = $section; + } else { + $this->sections[$section->id] = $section; + } + $this->props = $props; + } + + /** + * get section states + */ + public function getState(): array + { + $state = []; + foreach ($this->sections as $section) { + if(is_null($section)) { + $props = [ + ...$this->props + ]; + } else { + $props = [ + ...$this->props, + $section->type => $section->getState(), + ]; + } + $state[] = [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'sectionComponent' => $this->sectionComponent, + 'props' => $props, + ]; + } + return $state; + } +} diff --git a/classes/invitation/stepTypes/AcceptInvitationStep.php b/classes/invitation/stepTypes/AcceptInvitationStep.php new file mode 100644 index 00000000000..b14f93d50d6 --- /dev/null +++ b/classes/invitation/stepTypes/AcceptInvitationStep.php @@ -0,0 +1,197 @@ +invitationModel->userId) { + case !null: + $user = Repo::user()->get($invitation->invitationModel->userId); + if(!$user->getData('orcidAccessToken')) { + $steps[] = $this->verifyOrcidStep(); + $steps[] = $this->acceptInvitationReviewStep(); + } + break; + default: + $steps[] = $this->verifyOrcidStep(); + $steps[] = $this->userAccountDetailsStep(); + $steps[] = $this->userDetailsStep($context); + $steps[] = $this->acceptInvitationReviewStep(); + } + return $steps; + } + + /** + * user orcid verification step + */ + private function verifyOrcidStep(): \stdClass + { + $sections = new Sections( + 'userVerifyOrcid', + __('acceptInvitation.verifyOrcid.stepName'), + __('userInvitation.searchUser.stepDescription'), + 'popup', + 'AcceptInvitationVerifyOrcid' + ); + $sections->addSection( + null, + [ + 'validateFields' => [] + ] + ); + $step = new Step( + 'verifyOrcid', + __('acceptInvitation.verifyOrcid.stepName'), + __('acceptInvitation.verifyOrcid.stepDescription'), + __('acceptInvitation.verifyOrcid.stepLabel'), + __('userInvitation.verifyOrcid.nextButtonLabel'), + 'popup' + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } + + /** + * user account details step + */ + private function userAccountDetailsStep(): \stdClass + { + $sections = new Sections( + 'userCreateForm', + __('acceptInvitation.accountDetails.stepName'), + __('userInvitation.accountDetails.stepDescription'), + 'form', + 'AcceptInvitationUserAccountDetails' + ); + $sections->addSection( + null, + [ + 'validateFields' => [ + 'username', + 'password' + ] + ] + ); + $step = new Step( + 'userCreate', + __('acceptInvitation.accountDetails.stepName'), + __('acceptInvitation.accountDetails.stepDescription'), + __('acceptInvitation.accountDetails.stepLabel'), + __('acceptInvitation.accountDetails.nextButtonLabel'), + 'form' + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } + + /** + * user details form step + * + * @throws \Exception + */ + private function userDetailsStep(Context $context): \stdClass + { + $localeNames = $context->getSupportedFormLocaleNames(); + $locales = []; + foreach ($localeNames as $key => $name) { + $locales[] = [ + 'key' => $key, + 'label' => $name, + ]; + } + $sections = new Sections( + 'userCreateForm', + __('acceptInvitation.accountDetails.stepName'), + __('userInvitation.accountDetails.stepDescription'), + 'form', + 'AcceptInvitationUserDetailsForms' + ); + $sections->addSection( + new Form( + 'userDetails', + __('acceptInvitation.userDetails.form.name'), + __('acceptInvitation.userDetails.form.description'), + new AcceptUserDetailsForm('post', $locales), + ), + [ + 'validateFields' => [ + 'affiliation', + 'givenName', + 'familyName', + 'userCountry', + ] + ] + ); + $step = new Step( + 'userDetails', + __('acceptInvitation.userDetails.stepName'), + __('acceptInvitation.userDetails.stepDescription'), + __('acceptInvitation.userDetails.stepLabel'), + __('acceptInvitation.userDetails.nextButtonLabel'), + 'form' + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } + + /** + * review details and accept invitation step + * + * @throws \Exception + */ + private function acceptInvitationReviewStep(): \stdClass + { + $sections = new Sections( + 'userCreateRoles', + '', + '', + 'table', + 'AcceptInvitationReview' + ); + $sections->addSection( + null, + [ + 'validateFields' => [ + + ] + ] + ); + $step = new Step( + 'userCreateReview', + __('acceptInvitation.detailsReview.stepName'), + __('acceptInvitation.detailsReview.stepDescription'), + __('acceptInvitation.detailsReview.stepLabel'), + __('acceptInvitation.detailsReview.nextButtonLabel'), + 'review' + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } +} diff --git a/classes/invitation/stepTypes/InvitationStepTypes.php b/classes/invitation/stepTypes/InvitationStepTypes.php new file mode 100644 index 00000000000..6bb90f28feb --- /dev/null +++ b/classes/invitation/stepTypes/InvitationStepTypes.php @@ -0,0 +1,34 @@ +invitationSearchUser(); + } + $steps[] = $this->invitationDetailsForm($context); + $steps[] = $this->invitationInvitedEmail($context); + return $steps; + } + + /** + * create search user section + */ + private function invitationSearchUser(): stdClass + { + $sections = new Sections( + 'searchUserForm', + __('userInvitation.searchUser.stepName'), + __('userInvitation.searchUser.stepDescription'), + 'form', + 'UserInvitationSearchFormStep', + true + ); + $sections->addSection( + null, + [ + 'validateFields' => [] + ] + ); + $step = new Step( + 'searchUser', + __('userInvitation.searchUser.stepName'), + __('userInvitation.searchUser.stepDescription'), + __('userInvitation.searchUser.stepLabel'), + __('userInvitation.searchUser.nextButtonLabel'), + 'emptySection', + true + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } + + /** + * create user details form section + * + * @throws Exception + */ + private function invitationDetailsForm(Context $context): stdClass + { + $localeNames = $context->getSupportedFormLocaleNames(); + $locales = []; + foreach ($localeNames as $key => $name) { + $locales[] = [ + 'key' => $key, + 'label' => $name, + ]; + } + $sections = new Sections( + 'userDetails', + __('userInvitation.enterDetails.stepName'), + __('userInvitation.enterDetails.stepDescription'), + 'form', + 'UserInvitationDetailsFormStep' + ); + $sections->addSection( + new Form( + 'userDetails', + __('userInvitation.enterDetails.stepName'), + __('userInvitation.enterDetails.stepDescription'), + new UserDetailsForm('post', $locales, $context), + ), + [ + 'validateFields' => [ + 'inviteeEmail', + 'givenName', + 'familyName', + 'userGroupsToAdd', + ], + 'userGroups' => $this->getAllUserGroup($context) + ] + ); + $step = new Step( + 'userDetails', + __('userInvitation.enterDetails.stepName'), + __('userInvitation.enterDetails.stepDescription'), + __('userInvitation.enterDetails.stepLabel'), + __('userInvitation.enterDetails.nextButtonLabel'), + 'form' + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } + + /** + * create email composer for send invite + * + * @throws Exception + */ + private function invitationInvitedEmail(Context $context): stdClass + { + $sections = new Sections( + 'userInvitedEmail', + __('userInvitation.sendMail.stepLabel'), + __('userInvitation.sendMail.stepName'), + 'email', + 'UserInvitationEmailComposerStep' + ); + $fakeInvitation = $this->getFakeInvitation(); + $mailable = new UserRoleAssignmentInvitationNotify($context, $fakeInvitation); + $sections->addSection( + new Email( + 'userInvited', + __('userInvitation.sendMail.stepName'), + __('userInvitation.sendMail.stepDescription'), + [], + $mailable + ->sender(Application::get()->getRequest()->getUser()) + ->cc('') + ->bcc(''), + $context->getSupportedFormLocales(), + ), + [ + 'validateFields' => ['emailComposer'] + ] + ); + $step = new Step( + 'userInvited', + __('userInvitation.sendMail.stepName'), + __('userInvitation.sendMail.stepDescription'), + __('userInvitation.sendMail.stepLabel'), + __('userInvitation.sendMail.nextButtonLabel'), + 'email' + ); + $step->addSectionToStep($sections->getState()); + return $step->getState(); + } + + /** + * get all user groups + */ + private function getAllUserGroup(Context $context): array + { + $allUserGroups = []; + $userGroups = Repo::userGroup()->getCollector() + ->filterByContextIds([$context->getId()]) + ->getMany(); + foreach ($userGroups as $userGroup) { + $allUserGroups[] = [ + 'value' => (int) $userGroup->getId(), + 'label' => $userGroup->getLocalizedName(), + 'disabled' => false + ]; + } + return $allUserGroups; + } +} diff --git a/classes/invitation/steps/Step.php b/classes/invitation/steps/Step.php new file mode 100644 index 00000000000..adf35516516 --- /dev/null +++ b/classes/invitation/steps/Step.php @@ -0,0 +1,73 @@ +id = $id; + $this->name = $name; + $this->description = $description; + $this->stepLabel = $stepLabel; + $this->nextButtonLabel = $nextButtonLabel; + $this->type = $type; + $this->skipInvitationUpdate = $skipInvitationUpdate; + } + + /** + * Compile initial state data to pass to the frontend + */ + public function getState(): stdClass + { + $config = new stdClass(); + $config->id = $this->id; + $config->name = $this->name; + $config->description = $this->description; + $config->nextButtonLabel = $this->nextButtonLabel; + $config->skipInvitationUpdate = $this->skipInvitationUpdate; + $config->type = $this->type; + $config->stepLabel = $this->stepLabel; + $config->sections = $this->sections; + return $config; + } + + /** + * Add a step to the workflow + */ + public function addSectionToStep($sections): void + { + $this->sections = $sections; + } +} diff --git a/js/load.js b/js/load.js index d82c54741b6..1ee1f943881 100644 --- a/js/load.js +++ b/js/load.js @@ -106,6 +106,9 @@ import FieldSlider from '@/components/Form/fields/FieldSlider.vue'; // Panel components from UI Library import ListPanel from '@/components/ListPanel/ListPanel.vue'; +// Manager components +import UserInvitationManager from '@/managers/UserInvitationManager/UserInvitationManager.vue'; + // Helper for initializing and tracking Vue controllers import VueRegistry from './classes/VueRegistry.js'; @@ -216,6 +219,9 @@ VueRegistry.registerComponent('field-pub-id', FieldPubId); // Register ListPanel VueRegistry.registerComponent('PkpListPanel', ListPanel); +// Register Invitation Manager +VueRegistry.registerComponent('UserInvitationManager', UserInvitationManager); + const pinia = createPinia(); function pkpCreateVueApp(createAppArgs) { diff --git a/locale/en/invitation.po b/locale/en/invitation.po index 853e431cbf4..fa62e2d2a4d 100644 --- a/locale/en/invitation.po +++ b/locale/en/invitation.po @@ -82,6 +82,237 @@ msgstr "The {$attribute} field is required." msgid "invitation.userRoleAssignment.validation.error.userRoles.unexpectedProperties" msgstr "Unexpected properties found in the item at index {$attribute}: {$properties}." +msgid "invitation.step" +msgstr "STEP" + +msgid "invitation.header" +msgstr "Invitations" + +msgid "invitation.inviteToRole.btn" +msgstr "Invite to a role" + +msgid "invitation.wizard.pageTitle" +msgstr "Invite user to take a role" + +msgid "invitation.wizard.pageTitleDescription" +msgstr "You are inviting a user to take a role in OJS along with appearing in the journal masthead" + +msgid "userInvitation.enterDetailsLabel" +msgstr "Enter details" + +msgid "userInvitation.reviewAndInviteLabel" +msgstr "Review & invite for roles" + +msgid "userInvitation.searchUser.stepName" +msgstr "Search User" + +msgid "userInvitation.searchUser.stepLabel" +msgstr "{$step} - Search User" + +msgid "userInvitation.searchUser.nextButtonLabel" +msgstr "Search User" + +msgid "userInvitation.searchUser.stepDescription" +msgstr "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." + +msgid "userInvitation.emailField.description" +msgstr "e.g. aeinstein@example.com" + +msgid "userInvitation.usernameField.description" +msgstr "e.g. mickeymouse" + +msgid "userInvitation.orcidField.description" +msgstr "e.g. 0000-0000-0000-0000" + +msgid "userInvitation.enterDetails.stepName" +msgstr "Enter details" + +msgid "userInvitation.enterDetails.stepLabel" +msgstr "{$step} - Enter details and invite for roles" + +msgid "userInvitation.enterDetails.stepDescription" +msgstr "You can invite them to take up a role in OJS" + +msgid "userInvitation.enterDetails.nextButtonLabel" +msgstr "Save And Continue" + +msgid "invitation.role.selectRole" +msgstr "Select a new role" + +msgid "invitation.role.dateStart" +msgstr "Start Date" + +msgid "invitation.role.dateEnd" +msgstr "End Date" + +msgid "invitation.role.masthead" +msgstr "Journal Masthead" + +msgid "invitation.role.addRole.button" +msgstr "Add Another Role" + +msgid "userInvitation.roleTable.role" +msgstr "Role" + +msgid "userInvitation.roleTable.startDate" +msgstr "Start Date" + +msgid "userInvitation.roleTable.endDate" +msgstr "End Date" + +msgid "userInvitation.roleTable.journalMasthead" +msgstr "Journal Masthead" + +msgid "userInvitation.sendMail.stepName" +msgstr "Review & invite for roles" + +msgid "userInvitation.sendMail.nextButtonLabel" +msgstr "Invite user to the role" + +msgid "userInvitation.sendMail.stepDescription" +msgstr "Send the user an email to let them know about the invitation, next steps, journal GDPR polices and ORCiD verification" + +msgid "userInvitation.sendMail.stepLabel" +msgstr "{$step} - Modify email shared with the user" + +msgid "userInvitation.modal.title" +msgstr "Invitation Sent" + +msgid "userInvitation.modal.message" +msgstr "{$email} has been invited to new role in OJS.You can be updated about users on the User and Roles page, your ojs notification and/ or your email" + +msgid "userInvitation.modal.button" +msgstr "View All Users" + +msgid "invitation.role.removeRole.button" +msgstr "Remove Role" + +msgid "invitation.email.description" +msgstr "e.g. aeinstein@example.com" + +msgid "invitation.orcid.description" +msgstr "On accepting the invite, the user will be redirected to ORCID to verify their account, if they wish to." + +msgid "invitation.givenName.description" +msgstr "If you know the given name of the user, you can enter the information. However, this information can be changed by the user" + +msgid "invitation.familyName.description" +msgstr "If you know the family name of the user, you can enter the information. However, this information can be changed by the user" + +msgid "acceptInvitation.verifyOrcid.stepName" +msgstr "Verify ORCID iD" + +msgid "acceptInvitation.verifyOrcid.stepLabel" +msgstr "{$step} - Verify ORCID iD" + +msgid "acceptInvitation.verifyOrcid.stepDescription" +msgstr "You can choose to verify your ORCID iD ok skip it. If you chose to skip it now, You can verify your ORCID iD from your profile section in OJS later" + +msgid "acceptInvitation.verifyOrcid.nextButtonLabel" +msgstr "Save and continue" + +msgid "acceptInvitation.accountDetails.stepName" +msgstr "Create OJS account" + +msgid "acceptInvitation.accountDetails.stepLabel" +msgstr "{$step} - Create OJS account" + +msgid "acceptInvitation.accountDetails.stepDescription" +msgstr "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." + +msgid "acceptInvitation.accountDetails.nextButtonLabel" +msgstr "Save and continue" + +msgid "acceptInvitation.userDetails.stepName" +msgstr "Enter details" + +msgid "acceptInvitation.userDetails.stepLabel" +msgstr "{$step} - Enter details" + +msgid "acceptInvitation.userDetails.stepDescription" +msgstr "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." + +msgid "acceptInvitation.userDetails.nextButtonLabel" +msgstr "Save and continue" + +msgid "acceptInvitation.userDetails.form.name" +msgstr "Accept invitation user details form" + +msgid "acceptInvitation.userDetails.form.description" +msgstr "Please provide the following details to help us to manage your account" + +msgid "acceptInvitation.detailsReview.stepName" +msgstr "Review & create account" + +msgid "acceptInvitation.detailsReview.stepLabel" +msgstr "{$step} - Review & create account" + +msgid "acceptInvitation.detailsReview.stepDescription" +msgstr "Review details to start your new roles in OJS" + +msgid "acceptInvitation.detailsReview.nextButtonLabel" +msgstr "Accept And Continue to OJS" + +msgid "acceptInvitation.skipVerifyOrcid" +msgstr "Skip ORCID verification" + +msgid "acceptInvitation.verifyOrcid" +msgstr "Verify ORCID iD" + +msgid "acceptInvitation.usernameField.description" +msgstr "It should be 10 characters long and could be a combination of uppercase letters, lowercase letters or numbers" + +msgid "acceptInvitation.passwordField.description" +msgstr "It should be 12 characters long and should be a combination of uppercase letters, lowercase letters, numbers and symbols" + +msgid "acceptInvitation.privacyStatement.label" +msgstr "Yes, I agree to have my data collected and stored according to the" + +msgid "acceptInvitation.privacyStatement.btn" +msgstr "Privacy Statement" + +msgid "acceptInvitation.userDetailsForm.givenName.description" +msgstr "Also known as a forename or the first name, it is tha part of a personal name that identifies a preson" + +msgid "acceptInvitation.userDetailsForm.familyName.description" +msgstr "A surname, family name, or last name is the mostly hereditary portion of one's personal name that indicates one's family" + +msgid "acceptInvitation.userDetailsForm.affiliation.description" +msgstr "This is the institute you are affiliated with" + +msgid "acceptInvitation.userDetailsForm.countryOfAffiliation.description" +msgstr "This is a country in which the institute you are affiliated with is situated" + +msgid "acceptInvitation.userDetailsForm.countryOfAffiliation.label" +msgstr "County of affiliation" + +msgid "acceptInvitation.review.accountDetails" +msgstr "Account Details" + +msgid "acceptInvitation.review.userDetails" +msgstr "User Details" + +msgid "invitation.orcid.acceptInvitation.message" +msgstr "Not verified. You can verify your ORCID iD from your profile section in OJS" + +msgid "acceptInvitation.modal.title" +msgstr "You've been assigned a new role in OJS" + +msgid "acceptInvitation.modal.message" +msgstr "Congratulations on your new role in OJS! You might now have access to new options. If you need assistance navigating the system, please click on the “Help” buttons throughout the interface for guidance" + +msgid "acceptInvitation.modal.button" +msgstr "View All Submissions" + +msgid "invitation.tableHeader.name" +msgstr "Name" + +msgid "invitation.searchForm.emptyError" +msgstr "At least provide one search criteria." + +msgid "invitation.wizard.viewPageTitleDescription" +msgstr "You are viewing {$name}'s user details" + msgid "invitation.reviewerAccess.validation.error.reviewAssignmentId.notExisting" msgstr "The id {reviewAssignmentId} does not correspond to a valid review assignment" @@ -101,4 +332,46 @@ msgid "invitation.userRoleAssignment.userGroup.startDate.mustBeAfterToday" msgstr "This attribute must have a value equal or after today" msgid "invitation.validation.error.propertyProhibited" -msgstr "The :attribute field is prohibited" \ No newline at end of file +msgstr "The :attribute field is prohibited" + +msgid "invitation.cancelInvite.actionName" +msgstr "Cancel Invite" + +msgid "invitation.cancelInvite.title" +msgstr "Cancel Invitation" + +msgid "invitation.cancelInvite.message" +msgstr "Cancel the invitation sent to {$givenName} {$familyName} will deactivate acceptance link sent via email. Here are the invitation details: " + +msgid "invitation.masthead.show" +msgstr "Appear on the masthead" + +msgid "invitation.masthead.hidden" +msgstr "Dose not appear on the masthead" + +msgid "invitation.role.modifyRole.button" +msgstr "Modify Role" + +msgid "invitation.management.options" +msgstr "Invitation management options" + +msgid "userInvitation.cancel.message" +msgstr "Are you sure wnat to cancel this invitation ?" + +msgid "userInvitation.cancel.keepWorking" +msgstr "Keep Working" + +msgid "userInvitation.status.invited" +msgstr "Invited" + +msgid "userInvitation.search.userNotFound" +msgstr "The user does not have a role in this journal" + +msgid "userInvitation.search.userFound" +msgstr "The user already exists in the journal" + +msgid "userInvitation.edit.title" +msgstr "Edit Invitation" + +msgid "userInvitation.edit.message" +msgstr "If you edit the existing invitation or add a new role, the current invitation will be canceled and, a new one will be sent. Are you sure you want to proceed?" diff --git a/pages/invitation/InvitationHandler.php b/pages/invitation/InvitationHandler.php index 9874438dee5..3f8075a5b92 100644 --- a/pages/invitation/InvitationHandler.php +++ b/pages/invitation/InvitationHandler.php @@ -20,11 +20,16 @@ use APP\core\Request; use APP\facades\Repo; use APP\handler\Handler; +use APP\template\TemplateManager; +use PKP\core\PKPApplication; +use PKP\facades\Locale; use PKP\invitation\core\enums\InvitationAction; use PKP\invitation\core\Invitation; +use PKP\invitation\stepTypes\SendInvitationStep; class InvitationHandler extends Handler { + public $_isBackendPage = true; public const REPLY_PAGE = 'invitation'; public const REPLY_OP_ACCEPT = 'accept'; public const REPLY_OP_DECLINE = 'decline'; @@ -34,6 +39,7 @@ class InvitationHandler extends Handler */ public function accept(array $args, Request $request): void { + $this->setupTemplate($request); $invitation = $this->getInvitationByKey($request); $invitationHandler = $invitation->getInvitationActionRedirectController(); $invitationHandler->preRedirectActions(InvitationAction::ACCEPT); @@ -62,7 +68,17 @@ private function getInvitationByKey(Request $request): Invitation if (is_null($invitation)) { $request->getDispatcher()->handle404(); } + return $invitation; + } + + private function getInvitationById(Request $request, $id): Invitation + { + $invitation = Repo::invitation() + ->getById($id); + if (is_null($invitation)) { + $request->getDispatcher()->handle404(); + } return $invitation; } @@ -92,4 +108,130 @@ public static function getActionUrl(InvitationAction $action, Invitation $invita ] ); } + + public function invite($args, $request): void + { + $invitationMode = 'create'; + $invitationPayload = [ + 'userId' => null, + 'email' => '', + 'orcid' => '', + 'givenName' => '', + 'familyName' => '', + 'orcidValidation' => false, + 'userGroupsToAdd' => [ + [ + 'userGroupId' => null, + 'dateStart' => null, + 'dateEnd' => null, + 'masthead' => null, + ] + ], + 'currentUserGroups' => [], + 'userGroupsToRemove' => [], + 'emailComposer' => [ + 'body' => '', + 'subject' => '', + ] + ]; + $invitation = null; + $user = null; + if(!empty($args)) { + $invitation = $this->getInvitationById($request, $args[0]); + $payload = $invitation->getPayload()->toArray(); + $invitationModel = $invitation->invitationModel->toArray(); + + $invitationMode = 'edit'; + if($invitationModel['userId']){ + $user = Repo::user()->get($invitationModel['userId']); + } + $invitationPayload['userId'] = $invitationModel['userId']; + $invitationPayload['email'] = $invitationModel['email'] ?: $user->getEmail(); + $invitationPayload['orcid'] = $payload['orcid']; //$reviewer->getData('orcidAccessToken') + $invitationPayload['givenName'] = $user ? $user->getGivenName(null) : $payload['givenName']; + $invitationPayload['familyName'] = $user ? $user->getFamilyName(null) : $payload['familyName']; + $invitationPayload['affiliation'] = $user ? $user->getAffiliation(null) : $payload['affiliation']; + $invitationPayload['country'] = $user ? $user->getCountry() : $payload['userCountry']; + $invitationPayload['userGroupsToAdd'] = $payload['userGroupsToAdd']; + $invitationPayload['currentUserGroups'] = !$invitationModel['userId'] ? [] : $this->getUserUserGroups($invitationModel['userId']); + $invitationPayload['userGroupsToRemove'] = !$payload['userGroupsToRemove'] ? null : $payload['userGroupsToRemove']; + $invitationPayload['emailComposer'] = [ + 'emailBody'=>$payload['emailBody'], + 'emailSubject'=>$payload['emailSubject'], + ]; + } + $templateMgr = TemplateManager::getManager($request); + $breadcrumbs = $templateMgr->getTemplateVars('breadcrumbs'); + $this->setupTemplate($request); + $context = $request->getContext(); + $breadcrumbs[] = [ + 'id' => 'contexts', + 'name' => __('navigation.access'), + 'url' => $request + ->getDispatcher() + ->url( + $request, + PKPApplication::ROUTE_PAGE, + $request->getContext()->getPath(), + 'management', + 'settings', + ) + ]; + $breadcrumbs[] = [ + 'id' => 'invitationWizard', + 'name' => __('invitation.wizard.pageTitle'), + ]; + $steps = new SendInvitationStep(); + $templateMgr->setState([ + 'steps' => $steps->getSteps($invitation, $context), + 'emailTemplatesApiUrl' => $request + ->getDispatcher() + ->url( + $request, + Application::ROUTE_API, + $context->getData('urlPath'), + 'emailTemplates' + ), + 'primaryLocale' => $context->getData('primaryLocale'), + 'invitationType' => 'userRoleAssignment', + 'invitationPayload' => $invitationPayload, + 'invitationMode' => $invitationMode, + 'pageTitle' => $invitation ? + ( + $invitationPayload['givenName'][Locale::getLocale()] . ' ' + . $invitationPayload['familyName'][Locale::getLocale()]) + : __('invitation.wizard.pageTitle'), + 'pageTitleDescription' => $invitation ? + __( + 'invitation.wizard.viewPageTitleDescription', + ['name' => $invitationPayload['givenName'][Locale::getLocale()]] + ) + : __('invitation.wizard.pageTitleDescription'), + ]); + $templateMgr->assign([ + 'pageComponent' => 'PageOJS', + 'breadcrumbs' => $breadcrumbs, + 'pageWidth' => TemplateManager::PAGE_WIDTH_FULL, + ]); + $templateMgr->display('/invitation/userInvitation.tpl'); + } + + private function getUserUserGroups($id): array + { + $output = []; + $userGroups = Repo::userGroup()->userUserGroups($id); + foreach ($userGroups as $userGroup) { + $output[] = [ + 'id' => (int) $userGroup->getId(), + 'name' => $userGroup->getName(null), + 'abbrev' => $userGroup->getAbbrev(null), + 'roleId' => (int) $userGroup->getRoleId(), + 'showTitle' => (bool) $userGroup->getShowTitle(), + 'permitSelfRegistration' => (bool) $userGroup->getPermitSelfRegistration(), + 'permitMetadataEdit' => (bool) $userGroup->getPermitMetadataEdit(), + 'recommendOnly' => (bool) $userGroup->getRecommendOnly(), + ]; + } + return $output; + } } diff --git a/pages/invitation/index.php b/pages/invitation/index.php index cbe585b1269..d9d964fa01f 100644 --- a/pages/invitation/index.php +++ b/pages/invitation/index.php @@ -16,5 +16,6 @@ switch ($op) { case 'decline': case 'accept': + case 'invite': return new PKP\pages\invitation\InvitationHandler(); } diff --git a/templates/invitation/acceptInvitation.tpl b/templates/invitation/acceptInvitation.tpl new file mode 100644 index 00000000000..19e2341e217 --- /dev/null +++ b/templates/invitation/acceptInvitation.tpl @@ -0,0 +1,23 @@ + +{/block} diff --git a/templates/invitation/userInvitation.tpl b/templates/invitation/userInvitation.tpl new file mode 100644 index 00000000000..e28201ece18 --- /dev/null +++ b/templates/invitation/userInvitation.tpl @@ -0,0 +1,25 @@ +?php +{** + * templates/management/userInvitation.tpl + * + * Copyright (c) 2014-2024 Simon Fraser University + * Copyright (c) 2003-2024 John Willinsky + * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. + * + * @brief show create user invitation page to the users. + * + * @hook Template::Settings::access [] + *} +{extends file="layouts/backend.tpl"} +{block name="page"} + +{/block} diff --git a/templates/management/access.tpl b/templates/management/access.tpl index a39482b6569..0ffcc7b7cce 100644 --- a/templates/management/access.tpl +++ b/templates/management/access.tpl @@ -18,6 +18,7 @@ + {include file="management/accessUsers.tpl"}