UNPKG

box-ui-elements

Version:
506 lines (423 loc) • 19.6 kB
// @flow import * as React from 'react'; import getProp from 'lodash/get'; import noop from 'lodash/noop'; import { matchPath, type ContextRouter } from 'react-router-dom'; import { FEED_ITEM_TYPE_VERSION } from '../../constants'; import { getBadUserError } from '../../utils/error'; import type { WithAnnotatorContextProps } from '../common/annotator-context'; import type { BoxItem, User } from '../../common/types/core'; import { ViewType, FeedEntryType, type InternalSidebarNavigation, type InternalSidebarNavigationHandler, } from '../common/types/SidebarNavigation'; type Props = { ...ContextRouter, currentUser?: User, file: BoxItem, fileId: string, internalSidebarNavigation?: InternalSidebarNavigation, internalSidebarNavigationHandler?: InternalSidebarNavigationHandler, isOpen: boolean, onVersionChange: Function, routerDisabled?: boolean, } & WithAnnotatorContextProps; type SidebarPanelsRefType = { refresh: (shouldRefreshCache?: boolean) => void, }; export default function withSidebarAnnotations( WrappedComponent: React.ComponentType<Props>, ): React.ComponentType<Props> { class WithSidebarAnnotations extends React.Component<Props> { static defaultProps = { annotatorState: {}, getAnnotationsMatchPath: noop, getAnnotationsPath: noop, onVersionChange: noop, }; static displayName: ?string; props: Props; sidebarPanels: { current: SidebarPanelsRefType | null } = React.createRef(); constructor(props) { super(props); this.redirectDeeplinkedAnnotation(); } getInternalNavigationMatch = ( navigation: InternalSidebarNavigation, ): { params: { annotationId?: string, fileVersionId: string } } | null => { if ( !('activeFeedEntryType' in navigation) || navigation.activeFeedEntryType !== FeedEntryType.ANNOTATIONS || !navigation.fileVersionId ) { return null; } // Only include annotationId if it's defined (mirrors router behavior where missing optional params are omitted) const params = navigation.activeFeedEntryId !== undefined ? { fileVersionId: navigation.fileVersionId, annotationId: navigation.activeFeedEntryId, } : { fileVersionId: navigation.fileVersionId, }; return { params }; }; getInternalAnnotationsNavigation = ( fileVersionId?: string, annotationId?: string | null, ): InternalSidebarNavigation => { if (!fileVersionId) { return { sidebar: ViewType.ACTIVITY }; } return { sidebar: ViewType.ACTIVITY, activeFeedEntryType: FeedEntryType.ANNOTATIONS, activeFeedEntryId: annotationId || undefined, fileVersionId, }; }; redirectDeeplinkedAnnotation = () => { const { file, getAnnotationsPath, getAnnotationsMatchPath, history, internalSidebarNavigation, internalSidebarNavigationHandler, location, routerDisabled, } = this.props; const currentFileVersionId = getProp(file, 'file_version.id'); if (routerDisabled && internalSidebarNavigation && internalSidebarNavigationHandler) { // Use internal navigation when router is disabled const match = this.getInternalNavigationMatch(internalSidebarNavigation); const annotationId = getProp(match, 'params.annotationId'); const fileVersionId = getProp(match, 'params.fileVersionId'); if (fileVersionId && fileVersionId !== currentFileVersionId) { const correctedNavigation = this.getInternalAnnotationsNavigation( currentFileVersionId, annotationId, ); internalSidebarNavigationHandler(correctedNavigation, true); } } else { // Use router-based navigation const match = getAnnotationsMatchPath(location); const annotationId = getProp(match, 'params.annotationId'); const fileVersionId = getProp(match, 'params.fileVersionId'); if (fileVersionId && fileVersionId !== currentFileVersionId) { history.replace(getAnnotationsPath(currentFileVersionId, annotationId)); } } }; componentDidUpdate(prevProps: Props) { const { annotatorState, fileId, getAnnotationsMatchPath, internalSidebarNavigation, location, onVersionChange, routerDisabled, }: Props = this.props; const { annotatorState: prevAnnotatorState, fileId: prevFileId, internalSidebarNavigation: prevInternalSidebarNavigation, location: prevLocation, }: Props = prevProps; const { action, activeAnnotationId, annotation } = annotatorState; const { activeAnnotationId: prevActiveAnnotationId, annotation: prevAnnotation } = prevAnnotatorState; let fileVersionId; let prevFileVersionId; let match; if (routerDisabled && internalSidebarNavigation) { // Use internal navigation when router is disabled match = this.getInternalNavigationMatch(internalSidebarNavigation); const prevMatch = prevInternalSidebarNavigation ? this.getInternalNavigationMatch(prevInternalSidebarNavigation) : null; fileVersionId = getProp(match, 'params.fileVersionId'); prevFileVersionId = getProp(prevMatch, 'params.fileVersionId'); } else { // Use router-based navigation match = getAnnotationsMatchPath(location); const prevMatch = getAnnotationsMatchPath(prevLocation); fileVersionId = getProp(match, 'params.fileVersionId'); prevFileVersionId = getProp(prevMatch, 'params.fileVersionId'); } const isAnnotationsPath = !!match; const isTransitioningToAnnotationPath = activeAnnotationId && !isAnnotationsPath; const hasActiveAnnotationChanged = prevActiveAnnotationId !== activeAnnotationId; if (action === 'reply_create_start' || action === 'reply_create_end') { this.addAnnotationReply(); } if (action === 'reply_delete_start' || action === 'reply_delete_end') { this.deleteAnnotationReply(); } if (action === 'reply_update_start' || action === 'reply_update_end') { this.updateAnnotationReply(); } if (action === 'update_start' || action === 'update_end') { this.updateAnnotation(); } if (action === 'delete_start' || action === 'delete_end') { this.deleteAnnotation(); } if ((action === 'create_start' || action === 'create_end') && annotation && prevAnnotation !== annotation) { this.addAnnotation(); } // Active annotation id changed. If location is currently an annotation path or // if location is not currently an annotation path but the active annotation id // transitioned from falsy to truthy, update the location accordingly if (hasActiveAnnotationChanged && (isAnnotationsPath || isTransitioningToAnnotationPath)) { this.updateActiveAnnotation(); } if (fileVersionId && prevFileVersionId !== fileVersionId) { this.updateActiveVersion(); } if (prevFileId !== fileId) { // If the file id has changed, reset the current version id since the previous (possibly versioned) // location is no longer active onVersionChange(null); } } addAnnotation() { const { annotatorState: { action, annotation, meta: { requestId } = {} }, api, currentUser, file, fileId, } = this.props; if (!requestId) { return; } // TODO: need to address in follow on -- currentUser may be undefined here but is never fetched for sure until ActivitySidebar if (!currentUser) { throw getBadUserError(); } const feedAPI = api.getFeedAPI(false); const isPending = action === 'create_start'; const { items: hasItems } = feedAPI.getCachedItems(fileId) || {}; // If there are existing items in the cache for this file, then patch the cache with the new annotation // If there are no cache entry for feeditems, then it is assumed that it has not yet been fetched. if (hasItems) { feedAPI.addAnnotation(file, currentUser, annotation, requestId, isPending); } this.refreshActivitySidebar(); } addAnnotationReply() { const { annotatorState: { action, annotation: { id: annotationId }, annotationReply, meta: { requestId }, }, api, currentUser, file, } = this.props; if (!currentUser) { throw getBadUserError(); } const feedAPI = api.getFeedAPI(false); feedAPI.file = file; if (action === 'reply_create_start') { feedAPI.addPendingReply(annotationId, currentUser, { ...annotationReply, id: requestId }); } else { const { items: feedItems = [] } = feedAPI.getCachedItems(file.id) || {}; const annotationItem = feedItems.find(({ id }) => id === annotationId); if (!annotationItem) { return; } feedAPI.modifyFeedItemRepliesCountBy(annotationId, 1); feedAPI.updateReplyItem({ ...annotationReply, isPending: false }, annotationId, requestId); } this.refreshActivitySidebar(); } deleteAnnotation() { const { annotatorState: { action, annotation }, api, file, } = this.props; const feedAPI = api.getFeedAPI(false); feedAPI.file = file; if (action === 'delete_start') { feedAPI.updateFeedItem({ isPending: true }, annotation.id); } else { feedAPI.deleteFeedItem(annotation.id); } this.refreshActivitySidebar(); } deleteAnnotationReply() { const { annotatorState: { action, annotation: { id: annotationId }, annotationReply: { id: replyId }, }, api, file, } = this.props; const feedAPI = api.getFeedAPI(false); feedAPI.file = file; if (action === 'reply_delete_start') { feedAPI.updateReplyItem({ isPending: true }, annotationId, replyId); } else { const { items: feedItems = [] } = feedAPI.getCachedItems(file.id) || {}; const annotationItem = feedItems.find(({ id }) => id === annotationId); if (!annotationItem) { return; } // Check if the parent annotation has the reply currently visible and if so, remove it const replyItem = annotationItem.replies.find(({ id }) => id === replyId); if (replyItem) { feedAPI.deleteReplyItem(replyId, annotationId); } // Decrease the amount of replies by 1 feedAPI.modifyFeedItemRepliesCountBy(annotationId, -1); } this.refreshActivitySidebar(); } updateAnnotation() { const { annotatorState: { action, annotation }, api, file, } = this.props; const feedAPI = api.getFeedAPI(false); const isPending = action === 'update_start'; feedAPI.file = file; feedAPI.updateFeedItem({ ...annotation, isPending }, annotation.id); this.refreshActivitySidebar(); } updateAnnotationReply() { const { annotatorState: { action, annotation, annotationReply }, api, file, } = this.props; const feedAPI = api.getFeedAPI(false); const isPending = action === 'reply_update_start'; feedAPI.file = file; feedAPI.updateReplyItem({ ...annotationReply, isPending }, annotation.id, annotationReply.id); this.refreshActivitySidebar(); } updateActiveAnnotation = () => { const { annotatorState: { activeAnnotationFileVersionId, activeAnnotationId }, file, getAnnotationsMatchPath, getAnnotationsPath, history, internalSidebarNavigation, internalSidebarNavigationHandler, location, routerDisabled, } = this.props; const currentFileVersionId = getProp(file, 'file_version.id'); const defaultFileVersionId = activeAnnotationFileVersionId || currentFileVersionId; if (routerDisabled && internalSidebarNavigation && internalSidebarNavigationHandler) { // Use internal navigation when router is disabled const match = this.getInternalNavigationMatch(internalSidebarNavigation); const fileVersionId = getProp(match, 'params.fileVersionId', defaultFileVersionId); const newNavigationState = activeAnnotationId ? { open: true } : {}; // Update the navigation and open state if transitioning to an active annotation id, force the sidebar open const updatedNavigation = { ...this.getInternalAnnotationsNavigation(fileVersionId, activeAnnotationId), ...newNavigationState, }; internalSidebarNavigationHandler(updatedNavigation); } else { // Use router-based navigation const match = getAnnotationsMatchPath(location); const fileVersionId = getProp(match, 'params.fileVersionId', defaultFileVersionId); const newLocationState = activeAnnotationId ? { open: true } : location.state; // Update the location pathname and open state if transitioning to an active annotation id, force the sidebar open history.push({ pathname: getAnnotationsPath(fileVersionId, activeAnnotationId), state: newLocationState, }); } }; updateActiveVersion = () => { const { api, file, fileId, getAnnotationsMatchPath, getAnnotationsPath, history, internalSidebarNavigation, internalSidebarNavigationHandler, location, onVersionChange, routerDisabled, } = this.props; const feedAPI = api.getFeedAPI(false); const currentFileVersionId = getProp(file, 'file_version.id'); const { items: feedItems = [] } = feedAPI.getCachedItems(fileId) || {}; if (routerDisabled && internalSidebarNavigation && internalSidebarNavigationHandler) { // Use internal navigation when router is disabled const match = this.getInternalNavigationMatch(internalSidebarNavigation); const fileVersionId = getProp(match, 'params.fileVersionId'); const version = feedItems .filter(item => item.type === FEED_ITEM_TYPE_VERSION) .find(item => item.id === fileVersionId); if (version) { onVersionChange(version, { currentVersionId: currentFileVersionId, updateVersionToCurrent: () => { const currentVersionNavigation = this.getInternalAnnotationsNavigation(currentFileVersionId); internalSidebarNavigationHandler(currentVersionNavigation); }, }); } } else { // Use router-based navigation const match = getAnnotationsMatchPath(location); const fileVersionId = getProp(match, 'params.fileVersionId'); const version = feedItems .filter(item => item.type === FEED_ITEM_TYPE_VERSION) .find(item => item.id === fileVersionId); if (version) { onVersionChange(version, { currentVersionId: currentFileVersionId, updateVersionToCurrent: () => history.push(getAnnotationsPath(currentFileVersionId)), }); } } }; refreshActivitySidebar = () => { const { internalSidebarNavigation, isOpen, location, routerDisabled } = this.props; const { current } = this.sidebarPanels; let isActivity = false; if (routerDisabled && internalSidebarNavigation) { // Check if current navigation is pointing to activity sidebar isActivity = internalSidebarNavigation.sidebar === ViewType.ACTIVITY; } else { // Use router-based check const pathname = getProp(location, 'pathname', ''); isActivity = !!matchPath(pathname, '/activity'); } // If the activity sidebar is currently open, then force it to refresh with the updated data if (current && isActivity && isOpen) { current.refresh(false); } }; render() { return <WrappedComponent ref={this.sidebarPanels} {...this.props} />; } } const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; WithSidebarAnnotations.displayName = `WithSidebarAnnotations(${displayName})`; return WithSidebarAnnotations; }