diff --git a/public/globals.js b/public/globals.js index 5fcf4898c..d0dedf802 100644 --- a/public/globals.js +++ b/public/globals.js @@ -27,7 +27,6 @@ window.pkp = { * */ context: { - id: 1, apiBaseUrl: 'https://mock/index.php/publicknowledge/api/v1/', pageBaseUrl: 'https://mock/index.php/publicknowledge/', currentLocale: 'en', @@ -189,6 +188,14 @@ window.pkp = { 'admin.jobs.failed.action.redispatch': 'Try Again', 'admin.jobs.failed.action.redispatch.all': 'Requeue All Failed Jobs', 'admin.jobs.list.actions': 'Actions', + 'admin.workflow.email.userGroup.assign.unrestricted': + 'Mark as unrestricted', + 'admin.workflow.email.userGroup.limitAccess': + 'Limit access to specific roles', + 'admin.workflow.email.userGroup.limitAccess.template.note': + 'Select the roles that can access this template.', + 'admin.workflow.email.userGroup.unrestricted.template.note': + 'Unrestricted templates will be accessible to all roles.', 'article.article': 'Article', 'article.metadata': 'Metadata', 'author.users.contributor.principalContact': 'Primary Contact', diff --git a/src/managers/DiscussionManager/useDiscussionManagerForm.js b/src/managers/DiscussionManager/useDiscussionManagerForm.js index 77e994a74..2b00153aa 100644 --- a/src/managers/DiscussionManager/useDiscussionManagerForm.js +++ b/src/managers/DiscussionManager/useDiscussionManagerForm.js @@ -6,7 +6,6 @@ import {useUrl} from '@/composables/useUrl'; import {useLocalize} from '@/composables/useLocalize'; import {useFetch} from '@/composables/useFetch'; import {useCurrentUser} from '@/composables/useCurrentUser'; -import {useSubmission} from '@/composables/useSubmission'; import {useDiscussionMessages} from './useDiscussionMessages'; import { useDiscussionManagerStatusUpdater, @@ -35,7 +34,6 @@ export function useDiscussionManagerForm( const {updateStatus, startWorkItem} = useDiscussionManagerStatusUpdater( submission.id, ); - const {getCurrentReviewAssignments} = useSubmission(); const closeModal = inject('closeModal'); const currentUser = useCurrentUser(); @@ -73,35 +71,28 @@ export function useDiscussionManagerForm( function mapParticipantOptions(withSubLabel) { return (participant) => { - const userName = participant.userName && `(${participant.userName})`; - let label = `${participant.fullName} ${userName}`; + const username = participant.username && `(${participant.username})`; + let label = `${participant.fullName} ${username}`; - if (participant.userName === currentUser.getCurrentUserName()) { + if (participant.userId === currentUser.getCurrentUserId()) { label += ` (${t('common.me')})`; } + const participantRoles = participant.roles + ?.map((role) => role.name) + .join(t('common.commaListSeparator')); + return { label, - subLabel: withSubLabel ? participant.roleName : null, - value: participant.id, + subLabel: withSubLabel ? participantRoles : null, + value: participant.userId, }; }; } async function getAllParticipants() { - const reviewers = getCurrentReviewAssignments( - submission, - submissionStageId, - ).map((r) => ({ - fullName: r.reviewerFullName, - userName: r.reviewerUserName || '', - id: r.reviewerId, - roleName: t('user.role.reviewer'), - roleId: pkp.const.ROLE_ID_REVIEWER, - })); - const {apiUrl: participantsApiUrl} = useUrl( - `submissions/${encodeURIComponent(submission.id)}/participants/${submissionStageId}`, + `submissions/${encodeURIComponent(submission.id)}/stages/${submissionStageId}/tasks/participants`, ); const {data: participantsData, fetch: fetchParticipants} = @@ -109,50 +100,7 @@ export function useDiscussionManagerForm( await fetchParticipants(); - const isParticipant = participantsData.value?.some( - (p) => p.id === currentUser.getCurrentUserId(), - ); - - // If the current user is a site admin but not already in the participants list, add them as "Unassigned" - // This ensures site admins can always assign tasks to themselves - const siteAdmin = - currentUser.isCurrentUserSiteAdmin() && !isParticipant - ? [ - { - fullName: currentUser.getCurrentUserFullName(), - userName: currentUser.getCurrentUserName(), - id: currentUser.getCurrentUserId(), - roleName: t('submission.status.unassigned'), - roleId: pkp.const.ROLE_ID_SITE_ADMIN, - }, - ] - : []; - - const list = []; - participantsData.value?.forEach((participant) => { - participant.stageAssignments?.forEach((stageAssignment) => { - list.push({ - id: participant.id, - fullName: participant.fullName, - userName: participant.userName, - roleName: stageAssignment.stageAssignmentUserGroup.name, - roleId: stageAssignment.stageAssignmentUserGroup.roleId, - userGroupId: stageAssignment.stageAssignmentUserGroup.id, - }); - }); - }); - - list.sort((participantA, participantB) => { - // First, compare by roleId - if (participantA.roleId !== participantB.roleId) { - return participantA.roleId - participantB.roleId; - } - - // If roleIds are equal, compare by userGroupId - return participantA.userGroupId - participantB.userGroupId; - }); - - return list.concat(siteAdmin).concat(reviewers); + return participantsData.value || []; } const participantOptions = computed(() => @@ -162,7 +110,7 @@ export function useDiscussionManagerForm( const assigneeOptions = computed(() => { return participants.value .filter((participant) => - selectedParticipants.value.includes(participant.id), + selectedParticipants.value.includes(participant.userId), ) .map(mapParticipantOptions()); }); @@ -224,19 +172,16 @@ export function useDiscussionManagerForm( return []; } - const {apiUrl: taskTemplatesApiUrl} = useUrl('editTaskTemplates'); + const {apiUrl: taskTemplatesApiUrl} = useUrl( + `editTaskTemplates?stageId=${submissionStageId}`, + ); const {data: taskTemplatesData, fetch: fetchTaskTemplates} = useFetch(taskTemplatesApiUrl); fetchTaskTemplates(); - return computed( - () => - taskTemplatesData.value?.data?.filter( - (data) => data.stageId === submissionStageId, - ) || [], - ); + return computed(() => taskTemplatesData.value?.data || []); } async function setValuesFromTemplate(template) { @@ -254,10 +199,6 @@ export function useDiscussionManagerForm( isTask.value = templateData.value.type === pkp.const.EDITORIAL_TASK_TYPE_TASK; setValue('title', templateData.value.title); - setValue( - 'participants', - templateData.value.participants.map((p) => p.userId) || [], - ); setValue('taskInfoAdd', isTask.value); await nextTick(); // wait for the date due & assignee fields to re-render based on isTask value diff --git a/src/managers/TaskTemplateManager/TaskTemplateManager.stories.js b/src/managers/TaskTemplateManager/TaskTemplateManager.stories.js index ef38e321f..e7631da7c 100644 --- a/src/managers/TaskTemplateManager/TaskTemplateManager.stories.js +++ b/src/managers/TaskTemplateManager/TaskTemplateManager.stories.js @@ -63,9 +63,40 @@ const mswHandlers = [ }, ), - http.get('https://mock/index.php/publicknowledge/api/v1/userGroups', () => { - return HttpResponse.json(UserGroupMock); - }), + http.get( + 'https://mock/index.php/publicknowledge/api/v1/userGroups', + ({request}) => { + const url = new URL(request.url); + const stageId = url.searchParams.get('stageIds'); + switch (stageId) { + case '1': + return HttpResponse.json({ + items: UserGroupMock.WORKFLOW_STAGE_ID_SUBMISSION, + itemsMax: UserGroupMock.WORKFLOW_STAGE_ID_SUBMISSION.length, + }); + case '3': + return HttpResponse.json({ + items: UserGroupMock.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW, + itemsMax: UserGroupMock.WORKFLOW_STAGE_ID_EXTERNAL_REVIEW.length, + }); + case '4': + return HttpResponse.json({ + items: UserGroupMock.WORKFLOW_STAGE_ID_EDITING, + itemsMax: UserGroupMock.WORKFLOW_STAGE_ID_EDITING.length, + }); + case '5': + return HttpResponse.json({ + items: UserGroupMock.WORKFLOW_STAGE_ID_PRODUCTION, + itemsMax: UserGroupMock.WORKFLOW_STAGE_ID_PRODUCTION.length, + }); + default: + return HttpResponse.json({ + items: [], + itemsMax: 0, + }); + } + }, + ), ]; export const Default = { diff --git a/src/managers/TaskTemplateManager/useTaskTemplateManagerForm.js b/src/managers/TaskTemplateManager/useTaskTemplateManagerForm.js index cf97428cc..fffa6a28a 100644 --- a/src/managers/TaskTemplateManager/useTaskTemplateManagerForm.js +++ b/src/managers/TaskTemplateManager/useTaskTemplateManagerForm.js @@ -37,7 +37,10 @@ export function useTaskTemplateManagerForm({ const dataBody = { title: formData.title, stageId, - userGroupIds: formData.userGroupIds, + restrictToUserGroups: formData.restrictToUserGroups, + userGroupIds: formData.restrictToUserGroups + ? formData.userGroupIds + : null, include: formData.include, dueInterval: isTask.value ? formData.dueInterval : null, description: formData.description, @@ -78,8 +81,8 @@ export function useTaskTemplateManagerForm({ }; } - function getParticipantOptions() { - const {apiUrl: userGroupsApiUrl} = useUrl('userGroups'); + function getUserGroupOptions() { + const {apiUrl: userGroupsApiUrl} = useUrl(`userGroups?stageIds=${stageId}`); const {items: userGroupsData, fetch: fetchUserGroups} = useFetchPaginated( userGroupsApiUrl, @@ -202,14 +205,33 @@ export function useTaskTemplateManagerForm({ isRequired: true, }); + addFieldOptions('restrictToUserGroups', 'radio', { + groupId: 'details', + label: t('admin.workflow.email.userGroup.assign.unrestricted'), + description: t('admin.workflow.email.userGroup.unrestricted.template.note'), + name: 'restrictToUserGroups', + options: [ + { + value: false, + label: t('admin.workflow.email.userGroup.assign.unrestricted'), + }, + { + value: true, + label: t('admin.workflow.email.userGroup.limitAccess'), + }, + ], + value: !!taskTemplate?.restrictToUserGroups, + }); + addFieldOptions('userGroupIds', 'checkbox', { groupId: 'details', - label: t('editor.submission.stageParticipants'), - description: t('discussion.form.detailsParticipantsDescription'), + label: t('admin.workflow.email.userGroup.limitAccess'), + description: t('admin.workflow.email.userGroup.limitAccess.template.note'), name: 'userGroupIds', - options: getParticipantOptions(), + options: getUserGroupOptions(), value: taskTemplate?.userGroups?.map(({id}) => id) || [], isRequired: true, + showWhen: ['restrictToUserGroups', true], }); addGroup('taskInformation', { diff --git a/src/mockFactories/userGroupMock.js b/src/mockFactories/userGroupMock.js index 189f62626..467decf40 100644 --- a/src/mockFactories/userGroupMock.js +++ b/src/mockFactories/userGroupMock.js @@ -1,131 +1,227 @@ export const UserGroupMock = { - itemsMax: 18, - items: [ + WORKFLOW_STAGE_ID_SUBMISSION: [ { - id: 2, - roleId: null, + id: 3, + roleId: 16, + isDefault: true, + showTitle: true, + name: 'Journal editor', + }, + { + id: 5, + roleId: 17, + isDefault: true, + showTitle: true, + name: 'Section editor', + }, + { + id: 6, + roleId: 17, + isDefault: true, + showTitle: true, + name: 'Guest editor', + }, + { + id: 9, + roleId: 4097, + isDefault: true, + showTitle: true, + name: 'Funding coordinator', + }, + { + id: 14, + roleId: 65536, isDefault: true, showTitle: true, - name: 'Journal manager', + name: 'Author', }, + { + id: 15, + roleId: 65536, + isDefault: true, + showTitle: true, + name: 'Translator', + }, + ], + WORKFLOW_STAGE_ID_EXTERNAL_REVIEW: [ { id: 3, - roleId: null, + roleId: 16, + isDefault: true, + showTitle: true, + name: 'Journal editor', + }, + { + id: 5, + roleId: 17, + isDefault: true, + showTitle: true, + name: 'Section editor', + }, + { + id: 6, + roleId: 17, + isDefault: true, + showTitle: true, + name: 'Guest editor', + }, + { + id: 9, + roleId: 4097, + isDefault: true, + showTitle: true, + name: 'Funding coordinator', + }, + { + id: 14, + roleId: 65536, + isDefault: true, + showTitle: true, + name: 'Author', + }, + { + id: 15, + roleId: 65536, + isDefault: true, + showTitle: true, + name: 'Translator', + }, + { + id: 16, + roleId: 4096, + isDefault: true, + showTitle: true, + name: 'Reviewer', + }, + ], + WORKFLOW_STAGE_ID_EDITING: [ + { + id: 3, + roleId: 16, isDefault: true, showTitle: true, name: 'Journal editor', }, { id: 4, - roleId: null, + roleId: 16, isDefault: true, showTitle: true, name: 'Production editor', }, { id: 5, - roleId: null, + roleId: 17, isDefault: true, showTitle: true, name: 'Section editor', }, { id: 6, - roleId: null, + roleId: 17, isDefault: true, showTitle: true, name: 'Guest editor', }, { id: 7, - roleId: null, + roleId: 4097, isDefault: true, showTitle: true, name: 'Copyeditor', }, { - id: 8, - roleId: null, + id: 12, + roleId: 4097, isDefault: true, showTitle: true, - name: 'Designer', + name: 'Marketing and sales coordinator', }, { - id: 9, - roleId: null, + id: 14, + roleId: 65536, isDefault: true, showTitle: true, - name: 'Funding coordinator', + name: 'Author', }, { - id: 10, - roleId: null, + id: 15, + roleId: 65536, isDefault: true, showTitle: true, - name: 'Indexer', + name: 'Translator', }, + ], + WORKFLOW_STAGE_ID_PRODUCTION: [ { - id: 11, - roleId: null, + id: 3, + roleId: 16, isDefault: true, showTitle: true, - name: 'Layout Editor', + name: 'Journal editor', }, { - id: 12, - roleId: null, + id: 4, + roleId: 16, isDefault: true, showTitle: true, - name: 'Marketing and sales coordinator', + name: 'Production editor', }, { - id: 13, - roleId: null, + id: 5, + roleId: 17, isDefault: true, showTitle: true, - name: 'Proofreader', + name: 'Section editor', }, { - id: 14, - roleId: null, + id: 6, + roleId: 17, isDefault: true, showTitle: true, - name: 'Author', + name: 'Guest editor', }, { - id: 15, - roleId: null, + id: 8, + roleId: 4097, isDefault: true, showTitle: true, - name: 'Translator', + name: 'Designer', }, { - id: 16, - roleId: null, + id: 10, + roleId: 4097, isDefault: true, showTitle: true, - name: 'Reviewer', + name: 'Indexer', }, { - id: 17, - roleId: null, + id: 11, + roleId: 4097, isDefault: true, showTitle: true, - name: 'Reader', + name: 'Layout Editor', }, { - id: 18, - roleId: null, + id: 13, + roleId: 4097, isDefault: true, showTitle: true, - name: 'Subscription Manager', + name: 'Proofreader', }, { - id: 19, - roleId: null, + id: 14, + roleId: 65536, isDefault: true, showTitle: true, - name: 'Editorial Board Member', + name: 'Author', + }, + { + id: 15, + roleId: 65536, + isDefault: true, + showTitle: true, + name: 'Translator', }, ], };