Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<InlineNotice
title={formatMessage(messages.editForbiddenTitle)}
variant="warning"
variantIconAriaLabel={formatMessage(messages.inlineNoticeWarningAriaLabel)}
>
{formatMessage(forbiddenMessage)}
</InlineNotice>
);
}

if (isGroupLimit) {
return (
<InlineNotice
title={formatMessage(messages.groupExceedsLimitWarningTitle)}
variant="warning"
variantIconAriaLabel={formatMessage(messages.inlineNoticeWarningAriaLabel)}
>
{formatMessage(messages.groupExceedsLimitWarningMessage, { max: TASK_MAX_GROUP_ASSIGNEES })}
</InlineNotice>
);
}

return (
<InlineNotice
title={formatMessage(messages.taskErrorTitle)}
variant="error"
variantIconAriaLabel={formatMessage(messages.inlineNoticeErrorAriaLabel)}
>
{formatMessage(isEditMode ? messages.updateTaskErrorMessage : messages.createTaskErrorMessage)}
</InlineNotice>
);
};

export default TaskErrorNotice;
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,26 @@ 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';

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<FetchedAvatarUrls>;
fetchUsers: (query: string) => Promise<UserContactType[]>;
initialAssignees?: RuntimeAssignee[];
initialAssignees?: TaskAssignee[];
initialCompletionRule?: TaskCompletionRule;
initialDueDate?: Date | null;
initialMessage?: string;
isDisabled?: boolean;
onSubmit: (payload: TaskFormV2SubmitPayload) => void | Promise<void>;
taskId?: string;
taskType: TaskType;
};

Expand Down Expand Up @@ -74,6 +69,7 @@ const TaskFormV2 = ({
initialMessage = '',
isDisabled = false,
onSubmit,
taskId = '',
taskType,
}: TaskFormV2Props) => {
const { formatMessage } = useIntl();
Expand Down Expand Up @@ -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())) {
Expand Down Expand Up @@ -142,8 +157,10 @@ const TaskFormV2 = ({
data-resin-tasktype={taskType}
id={TASK_FORM_V2_ID}
onSubmit={handleFormSubmit}
{...resinTags}
>
<UserSelectorContainer
data-target-id="TaskFormV2-assigneeInput"
disabled={isDisabled}
error={assigneeError}
fetchAvatarUrls={fetchAvatarUrls}
Expand All @@ -157,6 +174,7 @@ const TaskFormV2 = ({
{shouldShowCompletionRule && (
<Checkbox.Item
checked={completionRule === TASK_COMPLETION_RULE_ANY}
data-target-id="TaskFormV2-completionRuleCheckbox"
disabled={isDisabled || isCompletionRuleDisabled}
label={formatMessage(messages.completionRuleCheckboxLabel)}
name="completionRule"
Expand All @@ -167,6 +185,7 @@ const TaskFormV2 = ({
/>
)}
<TextArea
data-target-id="TaskFormV2-messageInput"
disabled={isDisabled}
error={messageError}
label={formatMessage(messages.messageLabel)}
Expand All @@ -179,6 +198,7 @@ const TaskFormV2 = ({
<DatePicker
calendarAriaLabel={formatMessage(messages.datePickerCalendarAriaLabel)}
clearDatePickerAriaLabel={formatMessage(messages.datePickerClearAriaLabel)}
dataTargetId="TaskFormV2-dueDateInput"
isDisabled={isDisabled}
label={formatMessage(messages.dueDateLabel)}
minValue={minCalendarDate}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// Doubled class raises specificity above Blueprint Modal.Content size="medium" max-width default.
// Override Blueprint Modal.Content max-width; doubled selector raises specificity.
.bcs-NewTaskModal.bcs-NewTaskModal {
display: flex;
flex-direction: column;
max-width: 480px;
gap: var(--bp-space-040);
}

// Override Blueprint Modal.Body padding-top via doubled-class specificity.
.bcs-NewTaskModal-body.bcs-NewTaskModal-body {
position: relative;
display: flex;
flex-direction: column;
gap: var(--bp-space-040);
padding-top: 0;

// Pins the body to the modal's max-width so chip overflow inside the
Expand Down
Loading
Loading