UNPKG

metadata-based-explorer1

Version:
614 lines (546 loc) 18.4 kB
/** * @flow * @file Activity feed sidebar component * @author Box */ import * as React from 'react'; import debounce from 'lodash/debounce'; import noop from 'lodash/noop'; import flow from 'lodash/flow'; import messages from '../common/messages'; import { withAPIContext } from '../common/api-context'; import { withErrorBoundary } from '../common/error-boundary'; import { withFeatureConsumer, isFeatureEnabled } from '../common/feature-checking'; import { getBadUserError, getBadItemError } from '../../utils/error'; import API from '../../api'; import { withLogger } from '../common/logger'; import { mark } from '../../utils/performance'; import { EVENT_JS_READY } from '../common/logger/constants'; import ActivityFeed from './activity-feed'; import SidebarContent from './SidebarContent'; import AddTaskButton from './AddTaskButton'; import SidebarUtils from './SidebarUtils'; import { DEFAULT_COLLAB_DEBOUNCE, ORIGIN_ACTIVITY_SIDEBAR, SIDEBAR_VIEW_ACTIVITY, TASK_COMPLETION_RULE_ALL, } from '../../constants'; import type { TaskCompletionRule, TaskType, TaskNew, TaskUpdatePayload } from '../../common/types/tasks'; import './ActivitySidebar.scss'; type ExternalProps = { currentUser?: User, getUserProfileUrl?: GetProfileUrlCallback, onCommentCreate: Function, onCommentDelete: (comment: Comment) => any, onCommentUpdate: () => any, onTaskAssignmentUpdate: Function, onTaskCreate: Function, onTaskDelete: (id: string) => any, onTaskUpdate: () => any, } & ErrorContextProps; type PropsWithoutContext = { file: BoxItem, isDisabled: boolean, onVersionHistoryClick?: Function, translations?: Translations, } & ExternalProps & WithLoggerProps; type Props = { api: API, features: FeatureConfig, } & PropsWithoutContext; type State = { activityFeedError?: Errors, approverSelectorContacts: SelectorItems, currentUser?: User, currentUserError?: Errors, feedItems?: FeedItems, mentionSelectorContacts?: SelectorItems, }; export const activityFeedInlineError: Errors = { inlineError: { title: messages.errorOccured, content: messages.activityFeedItemApiError, }, }; const MARK_NAME_JS_READY = `${ORIGIN_ACTIVITY_SIDEBAR}_${EVENT_JS_READY}`; mark(MARK_NAME_JS_READY); class ActivitySidebar extends React.PureComponent<Props, State> { static defaultProps = { isDisabled: false, onCommentCreate: noop, onCommentDelete: noop, onCommentUpdate: noop, onTaskAssignmentUpdate: noop, onTaskCreate: noop, onTaskDelete: noop, onTaskUpdate: noop, }; constructor(props: Props) { super(props); // eslint-disable-next-line react/prop-types const { logger } = this.props; logger.onReadyMetric({ endMarkName: MARK_NAME_JS_READY, }); this.state = {}; } componentDidMount() { const { currentUser } = this.props; this.fetchFeedItems(true); this.fetchCurrentUser(currentUser); } /** * Fetches a Users info * * @private * @param {User} [user] - Box User. If missing, gets user that the current token was generated for. * @return {void} */ fetchCurrentUser(user?: User, shouldDestroy?: boolean = false): void { const { api, file } = this.props; if (!file) { throw getBadItemError(); } if (typeof user === 'undefined') { api.getUsersAPI(shouldDestroy).getUser( file.id, this.fetchCurrentUserSuccessCallback, this.fetchCurrentUserErrorCallback, ); } else { this.setState({ currentUser: user, currentUserError: undefined }); } } /** * Success callback for fetching feed items */ feedSuccessCallback = (): void => { this.fetchFeedItems(); }; /** * Error callback for fetching feed items * * @param {Error} e - the error which occured * @param {Error} code - the code for the error * @param {Object} contextInfo - the context info for the error */ feedErrorCallback = (e: ElementsXhrError, code: string, contextInfo?: Object) => { this.errorCallback(e, code, contextInfo); this.fetchFeedItems(); }; createTask = ( message: string, assignees: SelectorItems, taskType: TaskType, dueAt: ?string, completionRule: TaskCompletionRule, onSuccess: ?Function, onError: ?Function, ): void => { const { currentUser } = this.state; const { file, api } = this.props; if (!currentUser) { throw getBadUserError(); } const errorCallback = (e, code, contextInfo) => { if (onError) { onError(e, code, contextInfo); } this.feedErrorCallback(e, code, contextInfo); }; const successCallback = () => { if (onSuccess) { onSuccess(); } this.feedSuccessCallback(); }; api.getFeedAPI(false).createTaskNew( file, currentUser, message, assignees, taskType, dueAt, completionRule, successCallback, errorCallback, ); // need to load the pending item this.fetchFeedItems(); }; deleteTask = (task: TaskNew): void => { const { file, api, onTaskDelete } = this.props; api.getFeedAPI(false).deleteTaskNew( file, task, (taskId: string) => { this.feedSuccessCallback(); onTaskDelete(taskId); }, this.feedErrorCallback, ); // need to load the pending item this.fetchFeedItems(); }; updateTask = (task: TaskUpdatePayload, onSuccess: ?Function, onError: ?Function): void => { const { file, api, onTaskUpdate } = this.props; const errorCallback = (e, code) => { if (onError) { onError(e, code); } this.feedErrorCallback(e, code); }; const successCallback = () => { this.feedSuccessCallback(); if (onSuccess) { onSuccess(); } onTaskUpdate(); }; api.getFeedAPI(false).updateTaskNew(file, task, successCallback, errorCallback); // need to load the pending item this.fetchFeedItems(); }; updateTaskAssignment = (taskId: string, taskAssignmentId: string, status: TaskAssignmentStatus): void => { const { file, api } = this.props; api.getFeedAPI(false).updateTaskCollaborator( file, taskId, taskAssignmentId, status, this.feedSuccessCallback, this.feedErrorCallback, ); // need to load the pending item this.fetchFeedItems(); }; /** * Deletes a comment via the API. * * @param {Object} args - A subset of the comment * @return void */ deleteComment = ({ id, permissions }: { id: string, permissions: BoxItemPermission }): void => { const { file, api, onCommentDelete } = this.props; api.getFeedAPI(false).deleteComment( file, id, permissions, (comment: Comment) => { this.feedSuccessCallback(); onCommentDelete(comment); }, this.feedErrorCallback, ); // need to load the pending item this.fetchFeedItems(); }; updateComment = ( id: string, text: string, hasMention: boolean, permissions: BoxItemPermission, onSuccess: ?Function, onError: ?Function, ): void => { const { file, api, onCommentUpdate } = this.props; const errorCallback = (e, code) => { if (onError) { onError(e, code); } this.feedErrorCallback(e, code); }; const successCallback = () => { this.feedSuccessCallback(); if (onSuccess) { onSuccess(); } onCommentUpdate(); }; api.getFeedAPI(false).updateComment(file, id, text, hasMention, permissions, successCallback, errorCallback); // need to load the pending item this.fetchFeedItems(); }; /** * Posts a new comment to the API * * @param {string} text - The comment's text * @param {boolean} hasMention - The comment's text * @return {void} */ createComment = (text: string, hasMention: boolean): void => { const { file, api, onCommentCreate } = this.props; const { currentUser } = this.state; if (!currentUser) { throw getBadUserError(); } api.getFeedAPI(false).createComment( file, currentUser, text, hasMention, (comment: Comment) => { onCommentCreate(comment); this.feedSuccessCallback(); }, this.feedErrorCallback, ); // need to load the pending item this.fetchFeedItems(); }; /** * Deletes an app activity item via the API. * * @param {Object} args - A subset of the app activity * @return void */ deleteAppActivity = ({ id }: { id: string }): void => { const { file, api } = this.props; api.getFeedAPI(false).deleteAppActivity(file, id, this.feedSuccessCallback, this.feedErrorCallback); // need to load the pending item this.fetchFeedItems(); }; /** * Fetches the feed items for the sidebar * * @param {boolean} shouldDestroy true if the api factory should be destroyed */ fetchFeedItems(shouldRefreshCache: boolean = false, shouldDestroy: boolean = false) { const { file, api, features } = this.props; const shouldShowNewTasks = true; const shouldShowAppActivity = isFeatureEnabled(features, 'activityFeed.appActivity.enabled'); api.getFeedAPI(shouldDestroy).feedItems( file, shouldRefreshCache, this.fetchFeedItemsSuccessCallback, this.fetchFeedItemsErrorCallback, this.errorCallback, shouldShowNewTasks, shouldShowAppActivity, ); } /** * Handles a successful feed API fetch * * @private * @param {Array} feedItems - the feed items * @return {void} */ fetchFeedItemsSuccessCallback = (feedItems: FeedItems): void => { this.setState({ feedItems, activityFeedError: undefined }); }; /** * Handles a failed feed item fetch * * @private * @param {Error} e - API error * @return {void} */ fetchFeedItemsErrorCallback = (feedItems: FeedItems): void => { this.setState({ feedItems, activityFeedError: activityFeedInlineError, }); }; /** * Network error callback * * @private * @param {Error} error - Error object * @param {Error} code - the code for the error * @param {Object} contextInfo - the context info for the error * @return {void} */ errorCallback = (error: ElementsXhrError, code: string, contextInfo: Object = {}): void => { /* eslint-disable no-console */ console.error(error); /* eslint-enable no-console */ // eslint-disable-next-line react/prop-types this.props.onError(error, code, contextInfo); }; /** * User fetch success callback * * @private * @param {Object} currentUser - User info object * @return {void} */ fetchCurrentUserSuccessCallback = (currentUser: User): void => { this.setState({ currentUser, currentUserError: undefined }); }; /** * File approver contacts fetch success callback * * @private * @param {BoxItemCollection} collaborators - Collaborators response data * @return {void} */ getApproverContactsSuccessCallback = (collaborators: Collaborators): void => { const { entries } = collaborators; this.setState({ approverSelectorContacts: entries }); }; /** * File @mention contacts fetch success callback * * @private * @param {BoxItemCollection} collaborators - Collaborators response data * @return {void} */ getMentionContactsSuccessCallback = (collaborators: Collaborators): void => { const { entries } = collaborators; this.setState({ mentionSelectorContacts: entries }); }; /** * File @mention contacts fetch success callback * * @private * @param {string} searchStr - Search string to filter file collaborators by * @return {void} */ getApproverWithQuery = debounce( this.getCollaborators.bind(this, this.getApproverContactsSuccessCallback, this.errorCallback), DEFAULT_COLLAB_DEBOUNCE, ); /** * Fetches file @mention's * * @private * @param {string} searchStr - Search string to filter file collaborators by * @return {void} */ getMentionWithQuery = debounce( this.getCollaborators.bind(this, this.getMentionContactsSuccessCallback, this.errorCallback), DEFAULT_COLLAB_DEBOUNCE, ); /** * Fetches file collaborators * * @param {Function} successCallback - the success callback * @param {Function} errorCallback - the error callback * @param {string} searchStr - the search string * @return {void} */ getCollaborators(successCallback: Function, errorCallback: ElementsErrorCallback, searchStr: string): void { // Do not fetch without filter const { file, api } = this.props; if (!searchStr || searchStr.trim() === '') { return; } api.getFileCollaboratorsAPI(true).getFileCollaborators(file.id, successCallback, errorCallback, { filter_term: searchStr, }); } /** * Handles a failed file user info fetch * * @private * @param {ElementsXhrError} e - API error * @return {void} */ fetchCurrentUserErrorCallback = (e: ElementsXhrError, code: string) => { this.setState({ currentUser: undefined, currentUserError: { maskError: { errorHeader: messages.currentUserErrorHeaderMessage, errorSubHeader: messages.defaultErrorMaskSubHeaderMessage, }, }, }); this.errorCallback(e, code, { error: e, }); }; /** * Gets the user avatar URL * * @param {string} userId the user id * @param {string} fileId the file id * @return the user avatar URL string for a given user with access token attached */ getAvatarUrl = async (userId: string): Promise<?string> => { const { file, api } = this.props; return api.getUsersAPI(false).getAvatarUrlWithAccessToken(userId, file.id); }; onTaskModalClose = () => { this.setState({ approverSelectorContacts: [], }); }; refresh(): void { this.fetchFeedItems(true); } renderAddTaskButton = () => { const { isDisabled } = this.props; const { approverSelectorContacts } = this.state; const { getApproverWithQuery, getAvatarUrl, createTask, onTaskModalClose } = this; const props = { isDisabled, onTaskModalClose, }; const taskFormProps = { approverSelectorContacts, completionRule: TASK_COMPLETION_RULE_ALL, createTask, getApproverWithQuery, getAvatarUrl, id: '', message: '', approvers: [], }; return <AddTaskButton {...props} taskFormProps={taskFormProps} />; }; render() { const { file, isDisabled = false, onVersionHistoryClick, getUserProfileUrl } = this.props; const { currentUser, approverSelectorContacts, mentionSelectorContacts, feedItems, activityFeedError, currentUserError, } = this.state; return ( <SidebarContent className="bcs-activity" title={SidebarUtils.getTitleForView(SIDEBAR_VIEW_ACTIVITY)} actions={this.renderAddTaskButton()} > <ActivityFeed file={file} activityFeedError={activityFeedError} approverSelectorContacts={approverSelectorContacts} mentionSelectorContacts={mentionSelectorContacts} currentUser={currentUser} isDisabled={isDisabled} onAppActivityDelete={this.deleteAppActivity} onCommentCreate={this.createComment} onCommentDelete={this.deleteComment} onCommentUpdate={this.updateComment} onTaskCreate={this.createTask} onTaskDelete={this.deleteTask} onTaskUpdate={this.updateTask} onTaskModalClose={this.onTaskModalClose} onTaskAssignmentUpdate={this.updateTaskAssignment} getApproverWithQuery={this.getApproverWithQuery} getMentionWithQuery={this.getMentionWithQuery} onVersionHistoryClick={onVersionHistoryClick} getAvatarUrl={this.getAvatarUrl} getUserProfileUrl={getUserProfileUrl} feedItems={feedItems} currentUserError={currentUserError} /> </SidebarContent> ); } } export type ActivitySidebarProps = ExternalProps; export { ActivitySidebar as ActivitySidebarComponent }; export default flow([ withLogger(ORIGIN_ACTIVITY_SIDEBAR), withErrorBoundary(ORIGIN_ACTIVITY_SIDEBAR), withAPIContext, withFeatureConsumer, ])(ActivitySidebar);