diff --git a/i18n/en-US.properties b/i18n/en-US.properties index 36ae547e2f..4bd1bca90e 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -892,14 +892,20 @@ be.taskModalV2.assigneeFieldRequiredError = Required Field be.taskModalV2.assigneePlaceholder = Add an assignee or group # Label for the assignee combobox in the task modal be.taskModalV2.assigneeSelectorLabel = Select Assignees +# Label for the Cancel button in the task modal footer +be.taskModalV2.cancelButtonLabel = Cancel # aria-label for the task modal close button be.taskModalV2.close = Close # Label for the checkbox that switches a task to any-assignee completion be.taskModalV2.completionRuleCheckboxLabel = Only one assignee is required to complete this task # Title of the modal for creating an approval task be.taskModalV2.createApprovalTask = Create Approval Task +# Label for the Create button in the task modal footer (create mode) +be.taskModalV2.createButtonLabel = Create # Title of the modal for creating a general task be.taskModalV2.createGeneralTask = Create General Task +# Body text of the error notice when creating a task fails +be.taskModalV2.createTaskErrorMessage = An error occurred while creating this task. Please try again. # aria-label for the calendar inside the due-date date picker be.taskModalV2.datePickerCalendarAriaLabel = Due date calendar # aria-label for the clear button on the due-date date picker @@ -910,18 +916,40 @@ be.taskModalV2.datePickerNextMonthAriaLabel = Next month be.taskModalV2.datePickerOpenAriaLabel = Open due date calendar # aria-label for the previous-month button in the due-date date picker be.taskModalV2.datePickerPreviousMonthAriaLabel = Previous month -# Label for the due-date field in the task modal -be.taskModalV2.dueDateLabel = Due Date +# Label for the due-date field in the task modal; due date is optional +be.taskModalV2.dueDateLabel = Due Date (optional) # Title of the modal for editing an existing approval task be.taskModalV2.editApprovalTask = Modify Approval Task +# Warning shown when removing assignees from an approved task fails with 403 +be.taskModalV2.editApprovalTaskForbiddenMessage = Unable to remove assignee(s) because the task is now approved. +# Title of the warning notice when an edit partially fails (403 on update) +be.taskModalV2.editForbiddenTitle = Task Updated with Errors # Title of the modal for editing an existing general task be.taskModalV2.editGeneralTask = Modify General Task +# Warning shown when removing assignees from a completed task fails with 403 +be.taskModalV2.editGeneralTaskForbiddenMessage = Unable to remove assignee(s) because the task is now completed. +# Body of the warning notice when a group assignee exceeds the per-group maximum +be.taskModalV2.groupExceedsLimitWarningMessage = One or more groups can not receive this task as a group size cannot exceed the limit of {max} assignees per group. +# Title of the warning notice when a group assignee exceeds the per-group maximum +be.taskModalV2.groupExceedsLimitWarningTitle = Exceeded max assignees per group +# aria-label for the error variant icon in the task modal inline notice +be.taskModalV2.inlineNoticeErrorAriaLabel = Error +# aria-label for the warning variant icon in the task modal inline notice +be.taskModalV2.inlineNoticeWarningAriaLabel = Warning +# aria-label for the loading indicator on the submit button while a task is saving +be.taskModalV2.loadingAriaLabel = Loading # Error message when the task message field is empty on submit be.taskModalV2.messageFieldRequiredError = Required Field # Label for the message field in the task modal be.taskModalV2.messageLabel = Message # Placeholder text for the message field in the task modal be.taskModalV2.messagePlaceholder = Write a message +# Title of the error notice when creating or updating a task fails +be.taskModalV2.taskErrorTitle = Error +# Label for the Update button in the task modal footer (edit mode) +be.taskModalV2.updateButtonLabel = Update +# Body text of the error notice when updating an existing task fails +be.taskModalV2.updateTaskErrorMessage = An error occurred while modifying this task. Please try again. # Shown instead of todays date. be.today = today # Label for keywords/topics skill section in the preview sidebar diff --git a/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskErrorNotice.tsx b/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskErrorNotice.tsx new file mode 100644 index 0000000000..f6c6646543 --- /dev/null +++ b/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskErrorNotice.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; + +import { InlineNotice } from '@box/blueprint-web'; + +import { ERROR_CODE_GROUP_EXCEEDS_LIMIT, TASK_MAX_GROUP_ASSIGNEES, TASK_TYPE_GENERAL } from '../../../../constants'; + +import type { ElementsXhrError } from '../../../../common/types/api'; +import type { TaskType } from '../../../../common/types/tasks'; + +import messages from './messages'; + +export type TaskErrorNoticeProps = { + error: ElementsXhrError | undefined; + isEditMode: boolean; + taskType: TaskType; +}; + +const getErrorStatus = (error: ElementsXhrError): number | undefined => { + if ('status' in error && typeof error.status === 'number') { + return error.status; + } + const { response } = error as { response?: { status?: number } }; + return response?.status; +}; + +const getErrorCode = (error: ElementsXhrError): string | undefined => { + if ('code' in error && typeof error.code === 'string') { + return error.code; + } + const { response } = error as { response?: { data?: { code?: string } } }; + return response?.data?.code; +}; + +const TaskErrorNotice = ({ error, isEditMode, taskType }: TaskErrorNoticeProps) => { + const { formatMessage } = useIntl(); + + if (!error) { + return null; + } + + const status = getErrorStatus(error); + const code = getErrorCode(error); + const isForbiddenEdit = isEditMode && status === 403; + const isGroupLimit = code === ERROR_CODE_GROUP_EXCEEDS_LIMIT; + + if (isForbiddenEdit) { + const forbiddenMessage = + taskType === TASK_TYPE_GENERAL + ? messages.editGeneralTaskForbiddenMessage + : messages.editApprovalTaskForbiddenMessage; + return ( + + {formatMessage(forbiddenMessage)} + + ); + } + + if (isGroupLimit) { + return ( + + {formatMessage(messages.groupExceedsLimitWarningMessage, { max: TASK_MAX_GROUP_ASSIGNEES })} + + ); + } + + return ( + + {formatMessage(isEditMode ? messages.updateTaskErrorMessage : messages.createTaskErrorMessage)} + + ); +}; + +export default TaskErrorNotice; diff --git a/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskFormV2.tsx b/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskFormV2.tsx index b336a0dd71..b373b913e1 100644 --- a/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskFormV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/task-modal-v2/TaskFormV2.tsx @@ -11,7 +11,8 @@ import { TASK_COMPLETION_RULE_ALL, TASK_COMPLETION_RULE_ANY, TASK_EDIT_MODE_EDIT import type { TaskCompletionRule, TaskEditMode, TaskType } from '../../../../common/types/tasks'; -import { mapAssigneeToUserContact, mapUserContactToAssignee, type RuntimeAssignee } from './utils/contactMapping'; +import { mapAssigneeToUserContact, mapUserContactToAssignee } from './utils/contactMapping'; +import type { TaskAssignee, TaskFormV2SubmitPayload } from './types'; import messages from './messages'; @@ -19,23 +20,17 @@ import './TaskFormV2.scss'; export const TASK_FORM_V2_ID = 'task-form-v2'; -export type TaskFormV2SubmitPayload = { - assignees: RuntimeAssignee[]; - completionRule: TaskCompletionRule; - dueDate: Date | null; - message: string; -}; - export type TaskFormV2Props = { editMode?: TaskEditMode; fetchAvatarUrls: (contacts: UserContactType[]) => Promise; fetchUsers: (query: string) => Promise; - initialAssignees?: RuntimeAssignee[]; + initialAssignees?: TaskAssignee[]; initialCompletionRule?: TaskCompletionRule; initialDueDate?: Date | null; initialMessage?: string; isDisabled?: boolean; onSubmit: (payload: TaskFormV2SubmitPayload) => void | Promise; + taskId?: string; taskType: TaskType; }; @@ -74,6 +69,7 @@ const TaskFormV2 = ({ initialMessage = '', isDisabled = false, onSubmit, + taskId = '', taskType, }: TaskFormV2Props) => { const { formatMessage } = useIntl(); @@ -108,6 +104,25 @@ const TaskFormV2 = ({ ? formatMessage(messages.assigneeFieldRequiredError) : undefined; + const resinTags = React.useMemo(() => { + const initialIds = new Set(initialAssignees.map(a => a.target.id)); + const currentIds = new Set(selectedUsers.map(u => u.value)); + const added = selectedUsers.filter(u => !initialIds.has(u.value)); + const removed = initialAssignees.filter(a => !currentIds.has(a.target.id)); + const userCount = added.filter(u => u.type === 'user').length; + const groupCount = added.filter(u => u.type === 'group').length; + const submitDate = dueDate ? toSubmitDate(dueDate, originalDueDateTime).getTime() : undefined; + return { + 'data-resin-assigneesadded': added.map(u => u.value).join(','), + 'data-resin-assigneesremoved': removed.map(a => a.target.id).join(','), + 'data-resin-duedate': submitDate, + 'data-resin-numassigneesadded': userCount, + 'data-resin-numassigneesremoved': removed.length, + 'data-resin-numgroupsadded': groupCount, + 'data-resin-taskid': taskId, + }; + }, [dueDate, initialAssignees, originalDueDateTime, selectedUsers, taskId]); + const minCalendarDate = React.useMemo(() => { const todayDate = today(getLocalTimeZone()); if (initialDueDate && initialDueDate < todayDate.toDate(getLocalTimeZone())) { @@ -142,8 +157,10 @@ const TaskFormV2 = ({ data-resin-tasktype={taskType} id={TASK_FORM_V2_ID} onSubmit={handleFormSubmit} + {...resinTags} > )}