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}
>
)}