box-ui-elements-test
Version:
Box UI Elements
1,393 lines (1,262 loc) • 84.2 kB
JavaScript
/**
* @flow
* @file Helper for activity feed API's
* @author Box
*/
import noop from 'lodash/noop';
import uniqueId from 'lodash/uniqueId';
import type { MessageDescriptor } from 'react-intl';
import { getBadItemError, getBadUserError, getMissingItemTextOrStatus, isUserCorrectableError } from '../utils/error';
import commonMessages from '../elements/common/messages';
import messages from './messages';
import { sortFeedItems } from '../utils/sorter';
import { FEED_FILE_VERSIONS_FIELDS_TO_FETCH } from '../utils/fields';
import Base from './Base';
import AnnotationsAPI from './Annotations';
import CommentsAPI from './Comments';
import ThreadedCommentsAPI from './ThreadedComments';
import FileActivitiesAPI from './FileActivities';
import VersionsAPI from './Versions';
import TasksNewAPI from './tasks/TasksNew';
import GroupsAPI from './Groups';
import TaskCollaboratorsAPI from './tasks/TaskCollaborators';
import TaskLinksAPI from './tasks/TaskLinks';
import AppActivityAPI from './AppActivity';
import {
ACTION_TYPE_CREATED,
ACTION_TYPE_RESTORED,
ACTION_TYPE_TRASHED,
ERROR_CODE_CREATE_TASK,
ERROR_CODE_UPDATE_TASK,
ERROR_CODE_GROUP_EXCEEDS_LIMIT,
FEED_ITEM_TYPE_ANNOTATION,
FEED_ITEM_TYPE_COMMENT,
FEED_ITEM_TYPE_TASK,
FEED_ITEM_TYPE_VERSION,
FILE_ACTIVITY_TYPE_ANNOTATION,
FILE_ACTIVITY_TYPE_APP_ACTIVITY,
FILE_ACTIVITY_TYPE_COMMENT,
FILE_ACTIVITY_TYPE_TASK,
FILE_ACTIVITY_TYPE_VERSION,
HTTP_STATUS_CODE_CONFLICT,
IS_ERROR_DISPLAYED,
PERMISSION_CAN_VIEW_ANNOTATIONS,
PERMISSION_CAN_COMMENT,
TASK_NEW_APPROVED,
TASK_NEW_COMPLETED,
TASK_NEW_REJECTED,
TASK_NEW_NOT_STARTED,
TYPED_ID_FEED_PREFIX,
TASK_MAX_GROUP_ASSIGNEES,
} from '../constants';
import type {
TaskCompletionRule,
TaskCollabAssignee,
TaskCollabStatus,
TaskLink,
TaskNew,
TaskType,
TaskPayload,
TaskUpdatePayload,
} from '../common/types/tasks';
import type { ElementsXhrError, ErrorResponseData, APIOptions } from '../common/types/api';
import type {
SelectorItems,
SelectorItem,
UserMini,
GroupMini,
BoxItem,
BoxItemPermission,
BoxItemVersion,
FileVersions,
User,
} from '../common/types/core';
import type {
Annotation,
AnnotationPermission,
Annotations,
AppActivityItems,
BoxCommentPermission,
Comment,
CommentFeedItemType,
Comments,
FeedItem,
FeedItems,
FeedItemStatus,
FileActivity,
FileActivityTypes,
Task,
Tasks,
ThreadedComments as ThreadedCommentsType,
} from '../common/types/feed';
const TASK_NEW_INITIAL_STATUS = TASK_NEW_NOT_STARTED;
type FeedItemsCache = {
errors: ErrorResponseData[],
items: FeedItems,
};
type ErrorCallback = (e: ElementsXhrError, code: string, contextInfo?: Object) => void;
const getItemWithFilteredReplies = <T: { replies?: Array<Comment> }>(item: T, replyId: string): T => {
const { replies = [], ...rest } = item;
return { replies: replies.filter(({ id }) => id !== replyId), ...rest };
};
const getItemWithPendingReply = <T: { replies?: Array<Comment> }>(item: T, reply: Comment): T => {
const { replies = [], ...rest } = item;
return { replies: [...replies, reply], ...rest };
};
const parseReplies = (replies: Comment[]): Comment[] => {
const parsedReplies = [...replies];
return parsedReplies.map(reply => {
return { ...reply, tagged_message: reply.tagged_message || reply.message || '' };
});
};
export const getParsedFileActivitiesResponse = (response?: { entries: FileActivity[] }) => {
if (!response || !response.entries || !response.entries.length) {
return [];
}
const data = response.entries;
const parsedData: Array<Object> = data
.map(item => {
if (!item.source) {
return null;
}
const source = { ...item.source };
switch (item.activity_type) {
case FILE_ACTIVITY_TYPE_TASK: {
const taskItem = { ...source[FILE_ACTIVITY_TYPE_TASK] };
// UAA follows a lowercased enum naming convention, convert to uppercase to align with task api
if (taskItem.assigned_to?.entries) {
const assignedToEntries = taskItem.assigned_to.entries.map(entry => {
const assignedToEntry = { ...entry };
assignedToEntry.role = entry.role.toUpperCase();
assignedToEntry.status = entry.status.toUpperCase();
return assignedToEntry;
});
// $FlowFixMe Using the toUpperCase method makes Flow assume role and status is a string type, which is incompatible with string literal
taskItem.assigned_to.entries = assignedToEntries;
}
if (taskItem.completion_rule) {
taskItem.completion_rule = taskItem.completion_rule.toUpperCase();
}
if (taskItem.status) {
taskItem.status = taskItem.status.toUpperCase();
}
if (taskItem.task_type) {
taskItem.task_type = taskItem.task_type.toUpperCase();
}
// $FlowFixMe File Activities only returns a created_by user, Flow type fix is needed
taskItem.created_by = { target: taskItem.created_by };
return taskItem;
}
case FILE_ACTIVITY_TYPE_COMMENT: {
const commentItem = { ...source[FILE_ACTIVITY_TYPE_COMMENT] };
if (commentItem.replies && commentItem.replies.length) {
const replies = parseReplies(commentItem.replies);
commentItem.replies = replies;
}
commentItem.tagged_message = commentItem.tagged_message || commentItem.message || '';
return commentItem;
}
case FILE_ACTIVITY_TYPE_ANNOTATION: {
const annotationItem = { ...source[FILE_ACTIVITY_TYPE_ANNOTATION] };
if (annotationItem.replies && annotationItem.replies.length) {
const replies = parseReplies(annotationItem.replies);
annotationItem.replies = replies;
}
return annotationItem;
}
case FILE_ACTIVITY_TYPE_APP_ACTIVITY: {
const appActivityItem = { ...source[FILE_ACTIVITY_TYPE_APP_ACTIVITY] };
appActivityItem.created_at = appActivityItem.occurred_at;
return appActivityItem;
}
case FILE_ACTIVITY_TYPE_VERSION: {
const versionsItem = { ...source[FILE_ACTIVITY_TYPE_VERSION] };
versionsItem.type = FEED_ITEM_TYPE_VERSION;
if (versionsItem.action_by) {
const collaborators = {};
if (versionsItem.action_by.length === 1) {
versionsItem.uploader_display_name = versionsItem.action_by[0].name;
}
versionsItem.action_by.map(collaborator => {
collaborators[collaborator.id] = { ...collaborator };
return collaborator;
});
versionsItem.collaborators = collaborators;
}
if (versionsItem.end?.number) {
versionsItem.version_end = versionsItem.end.number;
versionsItem.id = versionsItem.end.id;
}
if (versionsItem.start?.number) {
versionsItem.version_start = versionsItem.start.number;
}
if (versionsItem.version_start === versionsItem.version_end) {
versionsItem.version_number = versionsItem.version_start;
if (
versionsItem.action_type === ACTION_TYPE_CREATED &&
versionsItem.start?.created_at &&
versionsItem.start?.created_by
) {
versionsItem.modified_at = versionsItem.start.created_at;
versionsItem.modified_by = { ...versionsItem.start.created_by };
}
if (
versionsItem.action_type === ACTION_TYPE_TRASHED &&
versionsItem.start?.trashed_at &&
versionsItem.start?.trashed_by
) {
versionsItem.trashed_at = versionsItem.start.trashed_at;
versionsItem.trashed_by = { ...versionsItem.start.trashed_by };
}
if (
versionsItem.action_type === ACTION_TYPE_RESTORED &&
versionsItem.start?.restored_at &&
versionsItem.start?.restored_by
) {
versionsItem.restored_at = versionsItem.start.restored_at;
versionsItem.restored_by = { ...versionsItem.start.restored_by };
}
}
return versionsItem;
}
default: {
return null;
}
}
})
.filter(item => !!item)
.reverse();
return parsedData;
};
class Feed extends Base {
/**
* @property {AnnotationsAPI}
*/
annotationsAPI: AnnotationsAPI;
/**
* @property {VersionsAPI}
*/
versionsAPI: VersionsAPI;
/**
* @property {CommentsAPI}
*/
commentsAPI: CommentsAPI;
/**
* @property {AppActivityAPI}
*/
appActivityAPI: AppActivityAPI;
/**
* @property {TasksNewAPI}
*/
tasksNewAPI: TasksNewAPI;
/**
* @property {TaskCollaboratorsAPI}
*/
taskCollaboratorsAPI: TaskCollaboratorsAPI[];
/**
* @property {TaskLinksAPI}
*/
taskLinksAPI: TaskLinksAPI[];
/**
* @property {ThreadedCommentsAPI}
*/
threadedCommentsAPI: ThreadedCommentsAPI;
/**
* @property {FileActivitiesAPI}
*/
fileActivitiesAPI: FileActivitiesAPI;
/**
* @property {BoxItem}
*/
file: BoxItem;
/**
* @property {ElementsXhrError}
*/
errors: ElementsXhrError[];
constructor(options: APIOptions) {
super(options);
this.taskCollaboratorsAPI = [];
this.taskLinksAPI = [];
this.errors = [];
}
/**
* Creates pending card on create_start action, then updates card on next call
* @param {BoxItem} file - The file to which the annotation is assigned
* @param {Object} currentUser - the user who performed the action
* @param {Annotation} annotation - the current annotation to be created
* @param {string} id - unique id for the incoming annotation
* @param {boolean} isPending - indicates the current creation process of the annotation
*/
addAnnotation(file: BoxItem, currentUser: User, annotation: Annotation, id: string, isPending: boolean): void {
if (!file.id) {
throw getBadItemError();
}
this.file = file;
// Add the pending interstitial card
if (isPending) {
const newAnnotation = {
...annotation,
created_by: currentUser,
id,
type: FEED_ITEM_TYPE_ANNOTATION,
};
this.addPendingItem(this.file.id, currentUser, newAnnotation);
return;
}
// Create action has completed, so update the existing pending item
this.updateFeedItem({ ...annotation, isPending: false }, id);
}
updateAnnotation = (
file: BoxItem,
annotationId: string,
text?: string,
status?: FeedItemStatus,
permissions: AnnotationPermission,
successCallback: (annotation: Annotation) => void,
errorCallback: ErrorCallback,
): void => {
if (!file.id) {
throw getBadItemError();
}
if (!text && !status) {
throw getMissingItemTextOrStatus();
}
this.annotationsAPI = new AnnotationsAPI(this.options);
this.file = file;
this.errorCallback = errorCallback;
const feedItemChanges = {};
if (text) {
feedItemChanges.message = text;
}
if (status) {
feedItemChanges.status = status;
}
this.updateFeedItem({ ...feedItemChanges, isPending: true }, annotationId);
this.annotationsAPI.updateAnnotation(
this.file.id,
annotationId,
permissions,
feedItemChanges,
(annotation: Annotation) => {
const { replies, total_reply_count, ...annotationBase } = annotation;
this.updateFeedItem(
{
// Do not update replies and total_reply_count props as their current values are not included in the response
...annotationBase,
isPending: false,
},
annotationId,
);
if (!this.isDestroyed()) {
successCallback(annotation);
}
},
(e: ErrorResponseData, code: string) => {
this.updateCommentErrorCallback(e, code, annotationId);
},
);
};
/**
* Error callback for updating a comment
*
* @param {ElementsXhrError} e - the error returned by the API
* @param {string} code - the error code
* @param {string} id - the id of either an annotation or comment
* @return {void}
*/
updateCommentErrorCallback = (e: ElementsXhrError, code: string, id: string) => {
this.updateFeedItem(this.createFeedError(messages.commentUpdateErrorMessage), id);
this.feedErrorCallback(true, e, code);
};
/**
* Error callback for updating a reply
*
* @param {ElementsXhrError} error - the error returned by the API
* @param {string} code - the error code
* @param {string} id - the id of the reply (comment)
* @param {string} parentId - the id of either the parent item (an annotation or comment)
* @return {void}
*/
updateReplyErrorCallback = (error: ElementsXhrError, code: string, id: string, parentId: string) => {
this.updateReplyItem(this.createFeedError(messages.commentUpdateErrorMessage), parentId, id);
this.feedErrorCallback(true, error, code);
};
/**
* Error callback for fetching replies
*
* @param {ElementsXhrError} error - the error returned by the API
* @param {string} code - the error code
* @param {string} id - the id of either an annotation or comment
* @return {void}
*/
fetchRepliesErrorCallback = (error: ElementsXhrError, code: string, id: string) => {
this.updateFeedItem(this.createFeedError(messages.repliesFetchErrorMessage), id);
this.feedErrorCallback(true, error, code);
};
deleteAnnotation = (
file: BoxItem,
annotationId: string,
permissions: AnnotationPermission,
successCallBack: Function,
errorCallback: Function,
): void => {
this.annotationsAPI = new AnnotationsAPI(this.options);
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.updateFeedItem({ isPending: true }, annotationId);
this.annotationsAPI.deleteAnnotation(
this.file.id,
annotationId,
permissions,
this.deleteFeedItem.bind(this, annotationId, successCallBack),
(error: ElementsXhrError, code: string) => {
// Reusing comment error handler since annotations are treated as comments to user
this.deleteCommentErrorCallback(error, code, annotationId);
},
);
};
/**
* Creates a key for the cache
*
* @param {string} id folder id
* @return {string} key
*/
getCacheKey(id: string): string {
return `${TYPED_ID_FEED_PREFIX}${id}`;
}
/**
* Gets the items from the cache
*
* @param {string} id the cache id
*/
getCachedItems(id: string): ?FeedItemsCache {
const cache = this.getCache();
const cacheKey = this.getCacheKey(id);
return cache.get(cacheKey);
}
/**
* Sets the items in the cache
*
* @param {string} id - the cache id
* @param {Array} items - the feed items to cache
*/
setCachedItems(id: string, items: FeedItems) {
const cache = this.getCache();
const cacheKey = this.getCacheKey(id);
cache.set(cacheKey, {
errors: this.errors,
items,
});
}
/**
* Gets the feed items
*
* @param {BoxItem} file - The file to which the task is assigned
* @param {boolean} shouldRefreshCache - Optionally updates the cache
* @param {Function} successCallback - the success callback which is called after data fetching is complete
* @param {Function} errorCallback - the error callback which is called after data fetching is complete if there was an error
* @param {Function} onError - the function to be called immediately after an error occurs
* @param {Object} [options]- feature flips, etc
* @param {Object} [options.shouldShowAppActivity] - feature flip the new app activity api
*/
feedItems(
file: BoxItem,
shouldRefreshCache: boolean,
successCallback: Function,
errorCallback: (feedItems: FeedItems, errors: ElementsXhrError[]) => void,
onError: ErrorCallback,
{
shouldShowAnnotations = false,
shouldShowAppActivity = false,
shouldShowReplies = false,
shouldShowTasks = true,
shouldShowVersions = true,
shouldUseUAA = false,
}: {
shouldShowAnnotations?: boolean,
shouldShowAppActivity?: boolean,
shouldShowReplies?: boolean,
shouldShowTasks?: boolean,
shouldShowVersions?: boolean,
shouldUseUAA?: boolean,
} = {},
): void {
const { id, permissions = {} } = file;
const cachedItems = this.getCachedItems(id);
if (cachedItems) {
const { errors, items } = cachedItems;
if (errors.length) {
errorCallback(items, errors);
} else {
successCallback(items);
}
if (!shouldRefreshCache) {
return;
}
}
this.file = file;
this.errors = [];
this.errorCallback = onError;
// Using the UAA File Activities endpoint replaces the need for these calls
const annotationsPromise =
!shouldUseUAA && shouldShowAnnotations
? this.fetchAnnotations(permissions, shouldShowReplies)
: Promise.resolve();
const commentsPromise = () => {
if (shouldUseUAA) {
return Promise.resolve();
}
return shouldShowReplies ? this.fetchThreadedComments(permissions) : this.fetchComments(permissions);
};
const tasksPromise = !shouldUseUAA && shouldShowTasks ? this.fetchTasksNew() : Promise.resolve();
const appActivityPromise =
!shouldUseUAA && shouldShowAppActivity ? this.fetchAppActivity(permissions) : Promise.resolve();
const versionsPromise = !shouldUseUAA && shouldShowVersions ? this.fetchVersions() : Promise.resolve();
const currentVersionPromise =
!shouldUseUAA && shouldShowVersions ? this.fetchCurrentVersion() : Promise.resolve();
const annotationActivityType =
shouldShowAnnotations && permissions[PERMISSION_CAN_VIEW_ANNOTATIONS]
? [FILE_ACTIVITY_TYPE_ANNOTATION]
: [];
const appActivityActivityType = shouldShowAppActivity ? [FILE_ACTIVITY_TYPE_APP_ACTIVITY] : [];
const taskActivityType = shouldShowTasks ? [FILE_ACTIVITY_TYPE_TASK] : [];
const versionsActivityType = shouldShowVersions ? [FILE_ACTIVITY_TYPE_VERSION] : [];
const commentActivityType = permissions[PERMISSION_CAN_COMMENT] ? [FILE_ACTIVITY_TYPE_COMMENT] : [];
const filteredActivityTypes = [
...annotationActivityType,
...appActivityActivityType,
...commentActivityType,
...taskActivityType,
...versionsActivityType,
];
const fileActivitiesPromise =
// Only fetch when activity types are explicitly stated
shouldUseUAA && filteredActivityTypes.length
? this.fetchFileActivities(permissions, filteredActivityTypes, shouldShowReplies)
: Promise.resolve();
const handleFeedItems = (feedItems: FeedItems) => {
if (!this.isDestroyed()) {
this.setCachedItems(id, feedItems);
if (this.errors.length) {
errorCallback(feedItems, this.errors);
} else {
successCallback(feedItems);
}
}
};
if (shouldUseUAA) {
fileActivitiesPromise.then(response => {
if (!response) {
return;
}
const parsedFeedItems = getParsedFileActivitiesResponse(response);
handleFeedItems(parsedFeedItems);
});
} else {
Promise.all([
versionsPromise,
currentVersionPromise,
commentsPromise(),
tasksPromise,
appActivityPromise,
annotationsPromise,
]).then(([versions: ?FileVersions, currentVersion: ?BoxItemVersion, ...feedItems]) => {
const versionsWithCurrent = currentVersion
? this.versionsAPI.addCurrentVersion(currentVersion, versions, this.file)
: undefined;
const sortedFeedItems = sortFeedItems(versionsWithCurrent, ...feedItems);
handleFeedItems(sortedFeedItems);
});
}
}
fetchAnnotations(permissions: BoxItemPermission, shouldFetchReplies?: boolean): Promise<?Annotations> {
this.annotationsAPI = new AnnotationsAPI(this.options);
return new Promise(resolve => {
this.annotationsAPI.getAnnotations(
this.file.id,
undefined,
permissions,
resolve,
this.fetchFeedItemErrorCallback.bind(this, resolve),
undefined,
undefined,
shouldFetchReplies,
);
});
}
/**
* Fetches the comments for a file
*
* @param {Object} permissions - the file permissions
* @return {Promise} - the file comments
*/
fetchComments(permissions: BoxItemPermission): Promise<?Comments> {
this.commentsAPI = new CommentsAPI(this.options);
return new Promise(resolve => {
this.commentsAPI.getComments(
this.file.id,
permissions,
resolve,
this.fetchFeedItemErrorCallback.bind(this, resolve),
);
});
}
/**
* Fetches a comment for a file
*
* @param {BoxItem} file - The file to which the comment belongs to
* @param {string} commentId - comment id
* @param {Function} successCallback
* @param {ErrorCallback} errorCallback
* @return {Promise} - the file comments
*/
fetchThreadedComment(
file: BoxItem,
commentId: string,
successCallback: (comment: Comment) => void,
errorCallback: ErrorCallback,
): Promise<?Comment> {
const { id, permissions } = file;
if (!id || !permissions) {
throw getBadItemError();
}
this.threadedCommentsAPI = new ThreadedCommentsAPI(this.options);
return new Promise(resolve => {
this.threadedCommentsAPI.getComment({
commentId,
errorCallback,
fileId: id,
permissions,
successCallback: this.fetchThreadedCommentSuccessCallback.bind(this, resolve, successCallback),
});
});
}
/**
* Callback for successful fetch of a comment
*
* @param {Function} resolve - resolve function
* @param {Function} successCallback - success callback
* @param {Comment} comment - comment data
* @return {void}
*/
fetchThreadedCommentSuccessCallback = (resolve: Function, successCallback: Function, comment: Comment): void => {
successCallback(comment);
resolve();
};
/**
* Fetches the comments with replies for a file
*
* @param {Object} permissions - the file permissions
* @return {Promise} - the file comments
*/
fetchThreadedComments(permissions: BoxItemPermission): Promise<?ThreadedCommentsType> {
this.threadedCommentsAPI = new ThreadedCommentsAPI(this.options);
return new Promise(resolve => {
this.threadedCommentsAPI.getComments({
errorCallback: this.fetchFeedItemErrorCallback.bind(this, resolve),
fileId: this.file.id,
permissions,
successCallback: resolve,
});
});
}
/**
* Fetches the file activities for a file
*
* @param {BoxItemPermission} permissions - the file permissions
* @param {FileActivityTypes[]} activityTypes - the activity types to filter by
* @param {boolean} shouldShowReplies - specify if replies should be included in the response
* @return {Promise} - the file comments
*/
fetchFileActivities(
permissions: BoxItemPermission,
activityTypes: FileActivityTypes[],
shouldShowReplies?: boolean = false,
): Promise<Object> {
this.fileActivitiesAPI = new FileActivitiesAPI(this.options);
return new Promise(resolve => {
this.fileActivitiesAPI.getActivities({
errorCallback: this.fetchFeedItemErrorCallback.bind(this, resolve),
fileID: this.file.id,
permissions,
successCallback: resolve,
activityTypes,
shouldShowReplies,
});
});
}
/**
* Fetches replies (comments) of a comment or annotation
*
* @param {BoxItem} file - The file to which the comment or annotation belongs to
* @param {string} commentFeedItemId - ID of the comment or annotation
* @param {CommentFeedItemType} commentFeedItemType - Type of the comment or annotation
* @param {Function} successCallback
* @param {ErrorCallback} errorCallback
* @return {void}
*/
fetchReplies(
file: BoxItem,
commentFeedItemId: string,
commentFeedItemType: CommentFeedItemType,
successCallback: (comments: Array<Comment>) => void,
errorCallback: ErrorCallback,
): void {
const { id, permissions } = file;
if (!id || !permissions) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.updateFeedItem({ isRepliesLoading: true }, commentFeedItemId);
const successCallbackFn = ({ entries }: ThreadedCommentsType) => {
this.updateFeedItem(
{ isRepliesLoading: false, replies: entries, total_reply_count: entries.length },
commentFeedItemId,
);
successCallback(entries);
};
const errorCallbackFn = (error: ErrorResponseData, code: string) => {
this.fetchRepliesErrorCallback(error, code, commentFeedItemId);
};
if (commentFeedItemType === FEED_ITEM_TYPE_ANNOTATION) {
this.annotationsAPI = new AnnotationsAPI(this.options);
this.annotationsAPI.getAnnotationReplies(
file.id,
commentFeedItemId,
permissions,
successCallbackFn,
errorCallbackFn,
);
} else if (commentFeedItemType === FEED_ITEM_TYPE_COMMENT) {
this.threadedCommentsAPI = new ThreadedCommentsAPI(this.options);
this.threadedCommentsAPI.getCommentReplies({
fileId: file.id,
commentId: commentFeedItemId,
permissions,
successCallback: successCallbackFn,
errorCallback: errorCallbackFn,
});
}
}
/**
* Fetches the versions for a file
*
* @return {Promise} - the file versions
*/
fetchVersions(): Promise<?FileVersions> {
this.versionsAPI = new VersionsAPI(this.options);
return new Promise(resolve => {
this.versionsAPI.getVersions(
this.file.id,
resolve,
this.fetchFeedItemErrorCallback.bind(this, resolve),
undefined,
undefined,
FEED_FILE_VERSIONS_FIELDS_TO_FETCH,
);
});
}
/**
* Fetches the current version for a file
*
* @return {Promise} - the file versions
*/
fetchCurrentVersion(): Promise<?BoxItemVersion> {
this.versionsAPI = new VersionsAPI(this.options);
return new Promise(resolve => {
const { file_version = {} } = this.file;
this.versionsAPI.getVersion(
this.file.id,
file_version.id,
resolve,
this.fetchFeedItemErrorCallback.bind(this, resolve),
);
});
}
/**
* Fetches the tasks for a file
*
* @return {Promise} - the feed items
*/
fetchTasksNew(): Promise<?Tasks> {
this.tasksNewAPI = new TasksNewAPI(this.options);
return new Promise(resolve => {
this.tasksNewAPI.getTasksForFile({
file: { id: this.file.id },
successCallback: resolve,
errorCallback: (err, code) => this.fetchFeedItemErrorCallback(resolve, err, code),
});
});
}
/**
* Error callback for fetching feed items.
* Should only call the error callback if the response is a 401, 429 or >= 500
*
* @param {Function} resolve - the function which will be called on error
* @param {Object} e - the axios error
* @param {string} code - the error code
* @return {void}
*/
fetchFeedItemErrorCallback(resolve: Function, e: ElementsXhrError, code: string) {
const { status } = e;
const shouldDisplayError = isUserCorrectableError(status);
this.feedErrorCallback(shouldDisplayError, e, code);
resolve();
}
/**
* Updates a task assignment
*
* @param {BoxItem} file - The file to which the task is assigned
* @param {string} taskId - ID of task to be updated
* @param {string} taskCollaboratorId - Task assignment ID
* @param {TaskCollabStatus} taskCollaboratorStatus - New task assignment status
* @param {Function} successCallback - the function which will be called on success
* @param {Function} errorCallback - the function which will be called on error
* @return {void}
*/
updateTaskCollaborator = (
file: BoxItem,
taskId: string,
taskCollaboratorId: string,
taskCollaboratorStatus: TaskCollabStatus,
successCallback: Function,
errorCallback: ErrorCallback,
): void => {
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.updateFeedItem({ isPending: true }, taskId);
const collaboratorsApi = new TaskCollaboratorsAPI(this.options);
this.taskCollaboratorsAPI.push(collaboratorsApi);
const taskCollaboratorPayload = {
id: taskCollaboratorId,
status: taskCollaboratorStatus,
};
const handleError = (e: ElementsXhrError, code: string) => {
let errorMessage;
switch (taskCollaboratorStatus) {
case TASK_NEW_APPROVED:
errorMessage = messages.taskApproveErrorMessage;
break;
case TASK_NEW_COMPLETED:
errorMessage = messages.taskCompleteErrorMessage;
break;
case TASK_NEW_REJECTED:
errorMessage = messages.taskRejectErrorMessage;
break;
default:
errorMessage = messages.taskCompleteErrorMessage;
}
this.updateFeedItem(this.createFeedError(errorMessage, messages.taskActionErrorTitle), taskId);
this.feedErrorCallback(true, e, code);
};
collaboratorsApi.updateTaskCollaborator({
file,
taskCollaborator: taskCollaboratorPayload,
successCallback: (taskCollab: TaskCollabAssignee) => {
this.updateTaskCollaboratorSuccessCallback(taskId, file, taskCollab, successCallback, handleError);
},
errorCallback: handleError,
});
};
/**
* Updates the task assignment state of the updated task
*
* @param {string} taskId - Box task id
* @param {TaskAssignment} updatedCollaborator - New task assignment from API
* @param {Function} successCallback - the function which will be called on success
* @return {void}
*/
updateTaskCollaboratorSuccessCallback = (
taskId: string,
file: { id: string },
updatedCollaborator: TaskCollabAssignee,
successCallback: Function,
errorCallback: Function,
): void => {
this.tasksNewAPI = new TasksNewAPI(this.options);
this.tasksNewAPI.getTask({
id: taskId,
file,
successCallback: task => {
this.updateFeedItem({ ...task, isPending: false }, taskId);
successCallback(updatedCollaborator);
},
errorCallback,
});
};
/**
* Updates a task in the new API
*
* @param {BoxItem} file - The file to which the task is assigned
* @param {string} task - The update task payload object
* @param {Function} successCallback - the function which will be called on success
* @param {Function} errorCallback - the function which will be called on error
* @return {void}
*/
updateTaskNew = async (
file: BoxItem,
task: TaskUpdatePayload,
successCallback: () => void = noop,
errorCallback: ErrorCallback = noop,
) => {
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.tasksNewAPI = new TasksNewAPI(this.options);
this.updateFeedItem({ isPending: true }, task.id);
try {
// create request for the size of each group by ID
// TODO: use async/await for both creating and editing tasks
const groupInfoPromises: Array<Promise<any>> = task.addedAssignees
.filter(
(assignee: SelectorItem<UserMini | GroupMini>) => assignee.item && assignee.item.type === 'group',
)
.map(assignee => assignee.id)
.map(groupId => {
return new GroupsAPI(this.options).getGroupCount({
file,
group: { id: groupId },
});
});
const groupCounts: Array<{ total_count: number }> = await Promise.all(groupInfoPromises);
const hasAnyGroupCountExceeded: boolean = groupCounts.some(
groupInfo => groupInfo.total_count > TASK_MAX_GROUP_ASSIGNEES,
);
const warning = {
code: ERROR_CODE_GROUP_EXCEEDS_LIMIT,
type: 'warning',
};
if (hasAnyGroupCountExceeded) {
this.feedErrorCallback(false, warning, ERROR_CODE_GROUP_EXCEEDS_LIMIT);
return;
}
await new Promise((resolve, reject) => {
this.tasksNewAPI.updateTaskWithDeps({
file,
task,
successCallback: resolve,
errorCallback: reject,
});
});
await new Promise((resolve, reject) => {
this.tasksNewAPI.getTask({
file,
id: task.id,
successCallback: (taskData: Task) => {
this.updateFeedItem(
{
...taskData,
isPending: false,
},
task.id,
);
resolve();
},
errorCallback: (e: ElementsXhrError) => {
this.updateFeedItem({ isPending: false }, task.id);
this.feedErrorCallback(false, e, ERROR_CODE_UPDATE_TASK);
reject();
},
});
});
// everything succeeded, so call the passed in success callback
if (!this.isDestroyed()) {
successCallback();
}
} catch (e) {
this.updateFeedItem({ isPending: false }, task.id);
this.feedErrorCallback(false, e, ERROR_CODE_UPDATE_TASK);
}
};
/**
* Deletes a comment.
*
* @param {BoxItem} file - The file to which the comment belongs to
* @param {string} commentId - Comment ID
* @param {BoxCommentPermission} permissions - Permissions for the comment
* @param {Function} successCallback - the function which will be called on success
* @param {Function} errorCallback - the function which will be called on error
* @return {void}
*/
deleteComment = (
file: BoxItem,
commentId: string,
permissions: BoxCommentPermission,
successCallback: Function,
errorCallback: ErrorCallback,
): void => {
this.commentsAPI = new CommentsAPI(this.options);
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.updateFeedItem({ isPending: true }, commentId);
this.commentsAPI.deleteComment({
file,
commentId,
permissions,
successCallback: this.deleteFeedItem.bind(this, commentId, successCallback),
errorCallback: (e: ElementsXhrError, code: string) => {
this.deleteCommentErrorCallback(e, code, commentId);
},
});
};
/**
* Deletes a threaded comment (using ThreadedComments API).
*
* @param {BoxItem} file - The file to which the comment belongs to
* @param {string} commentId - Comment ID
* @param {BoxCommentPermission} permissions - Permissions for the comment
* @param {Function} successCallback - the function which will be called on success
* @param {Function} errorCallback - the function which will be called on error
* @return {void}
*/
deleteThreadedComment = (
file: BoxItem,
commentId: string,
permissions: BoxCommentPermission,
successCallback: Function,
errorCallback: ErrorCallback,
): void => {
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.updateFeedItem({ isPending: true }, commentId);
this.threadedCommentsAPI = new ThreadedCommentsAPI(this.options);
this.threadedCommentsAPI.deleteComment({
fileId: file.id,
commentId,
permissions,
successCallback: this.deleteFeedItem.bind(this, commentId, successCallback),
errorCallback: (e: ElementsXhrError, code: string) => {
this.deleteCommentErrorCallback(e, code, commentId);
},
});
};
/**
* Deletes a reply (using ThreadedComments API).
*
* @param {BoxItem} file - The file to which the comment belongs to
* @param {string} id - id of the reply (comment)
* @param {string} parentId - id of the parent feed item
* @param {BoxCommentPermission} permissions - Permissions for the comment
* @param {Function} successCallback - the function which will be called on success
* @param {Function} errorCallback - the function which will be called on error
* @return {void}
*/
deleteReply = (
file: BoxItem,
id: string,
parentId: string,
permissions: BoxCommentPermission,
successCallback: () => void,
errorCallback: ErrorCallback,
): void => {
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
this.updateReplyItem({ isPending: true }, parentId, id);
this.threadedCommentsAPI = new ThreadedCommentsAPI(this.options);
this.threadedCommentsAPI.deleteComment({
fileId: file.id,
commentId: id,
permissions,
successCallback: this.deleteReplySuccessCallback.bind(this, id, parentId, successCallback),
errorCallback: (e: ElementsXhrError, code: string) => {
this.deleteReplyErrorCallback(e, code, id, parentId);
},
});
};
/**
* Callback for successful deletion of a reply.
*
* @param {string} id - ID of the reply
* @param {string} parentId - ID of the parent feed item
* @param {Function} successCallback - success callback
* @return {void}
*/
deleteReplySuccessCallback = (id: string, parentId: string, successCallback: Function): void => {
this.modifyFeedItemRepliesCountBy(parentId, -1);
this.deleteReplyItem(id, parentId, successCallback);
};
/**
* Error callback for deleting a comment
*
* @param {ElementsXhrError} e - the error returned by the API
* @param {string} code - the error code
* @param {string} commentId - the comment id
* @return {void}
*/
deleteCommentErrorCallback = (e: ElementsXhrError, code: string, commentId: string) => {
this.updateFeedItem(this.createFeedError(messages.commentDeleteErrorMessage), commentId);
this.feedErrorCallback(true, e, code);
};
/**
* Error callback for deleting a reply
*
* @param {ElementsXhrError} error - the error returned by the API
* @param {string} code - the error code
* @param {string} id - the reply (comment) id
* @param {string} parentId - the comment id of the parent feed item
* @return {void}
*/
deleteReplyErrorCallback = (error: ElementsXhrError, code: string, id: string, parentId: string) => {
this.updateReplyItem(this.createFeedError(messages.commentDeleteErrorMessage), parentId, id);
this.feedErrorCallback(true, error, code);
};
/**
* Creates a task.
*
* @param {BoxItem} file - The file to which the task is assigned
* @param {Object} currentUser - the user who performed the action
* @param {string} message - Task text
* @param {Array} assignees - List of assignees
* @param {number} dueAt - Task's due date
* @param {Function} successCallback - the function which will be called on success
* @param {Function} errorCallback - the function which will be called on error
* @return {void}
*/
createTaskNew = (
file: BoxItem,
currentUser: User,
message: string,
assignees: SelectorItems<>,
taskType: TaskType,
dueAt: ?string,
completionRule: TaskCompletionRule,
successCallback: Function,
errorCallback: ErrorCallback,
): void => {
if (!file.id) {
throw getBadItemError();
}
this.file = file;
this.errorCallback = errorCallback;
const uuid = uniqueId('task_');
let dueAtString;
if (dueAt) {
const dueAtDate: Date = new Date(dueAt);
dueAtString = dueAtDate.toISOString();
}
// TODO: make pending task generator a function
const pendingTask: TaskNew = {
created_by: {
type: 'task_collaborator',
target: currentUser,
id: uniqueId(),
role: 'CREATOR',
status: TASK_NEW_INITIAL_STATUS,
},
completion_rule: completionRule,
created_at: new Date().toISOString(),
due_at: dueAtString,
id: uuid,
description: message,
type: FEED_ITEM_TYPE_TASK,
assigned_to: {
entries: assignees.map((assignee: SelectorItem<UserMini | GroupMini>) => ({
id: uniqueId(),
target: { ...assignee, avatar_url: '', type: 'user' },
status: TASK_NEW_INITIAL_STATUS,
permissions: {
can_delete: false,
can_update: false,
},
role: 'ASSIGNEE',
type: 'task_collaborator',
})),
limit: 10,
next_marker: null,
},
permissions: {
can_update: false,
can_delete: false,
can_create_task_collaborator: false,
can_create_task_link: false,
},
task_links: {
entries: [
{
id: uniqueId(),
type: 'task_link',
target: {
type: 'file',
...file,
},
permissions: {
can_delete: false,
can_update: false,
},
},
],
limit: 1,
next_marker: null,
},
task_type: taskType,
status: TASK_NEW_NOT_STARTED,
};
const taskPayload: TaskPayload = {
description: message,
due_at: dueAtString,
task_type: taskType,
completion_rule: completionRule,
};
// create request for the size of each group by ID
const groupInfoPromises: Array<Promise<any>> = assignees
.filter((assignee: SelectorItem<UserMini | GroupMini>) => (assignee.item && assignee.item.type) === 'group')
.map(assignee => assignee.id)
.map(groupId => {
return new GroupsAPI(this.options).getGroupCount({
file,
group: { id: groupId },
});
});
// Fetch each group size in parallel --> return an array of group sizes
Promise.all(groupInfoPromises)
.then((groupCounts: Array<{ total_count: number }>) => {
const hasAnyGroupCountExceeded: boolean = groupCounts.some(
groupInfo => groupInfo.total_count > TASK_MAX_GROUP_ASSIGNEES,
);
const warning = {
code: ERROR_CODE_GROUP_EXCEEDS_LIMIT,
type: 'warning',
};
if (hasAnyGroupCountExceeded) {
this.feedErrorCallback(false, warning, ERROR_CODE_GROUP_EXCEEDS_LIMIT);
return;
}
this.tasksNewAPI = new TasksNewAPI(this.options);
this.tasksNewAPI.createTaskWithDeps({
file,
task: taskPayload,
assignees,
successCallback: (taskWithDepsData: any) => {
this.addPendingItem(this.file.id, currentUser, pendingTask);
this.updateFeedItem(
{
...taskWithDepsData,
task_links: {
entries: taskWithDepsData.task_links,
next_marker: null,
limit: 1,
},
assigned_to: {
entries: taskWithDepsData.assigned_to,
next_marker: null,
limit: taskWithDepsData.assigned_to.length,
},
isPending: false,