UNPKG

box-ui-elements-mlh

Version:
393 lines (359 loc) 12 kB
/** * @flow * @file Content Sidebar Container * @author Box */ import 'regenerator-runtime/runtime'; import * as React from 'react'; import noop from 'lodash/noop'; import flow from 'lodash/flow'; import type { RouterHistory } from 'react-router-dom'; import API from '../../api'; import APIContext from '../common/api-context'; import Internationalize from '../common/Internationalize'; import Sidebar from './Sidebar'; import NavRouter from '../common/nav-router'; import SidebarUtils from './SidebarUtils'; import { DEFAULT_HOSTNAME_API, CLIENT_NAME_CONTENT_SIDEBAR, ORIGIN_CONTENT_SIDEBAR } from '../../constants'; import { EVENT_JS_READY } from '../common/logger/constants'; import { mark } from '../../utils/performance'; import { SIDEBAR_FIELDS_TO_FETCH } from '../../utils/fields'; import { withErrorBoundary } from '../common/error-boundary'; import { withFeatureProvider } from '../common/feature-checking'; import { withLogger } from '../common/logger'; import type { DetailsSidebarProps } from './DetailsSidebar'; import type { ActivitySidebarProps } from './ActivitySidebar'; import type { MetadataSidebarProps } from './MetadataSidebar'; import type { VersionsSidebarProps } from './versions'; import type { WithLoggerProps } from '../../common/types/logging'; import type { ElementsXhrError, RequestOptions, ErrorContextProps } from '../../common/types/api'; import type { MetadataEditor } from '../../common/types/metadata'; import type { StringMap, Token, User, BoxItem } from '../../common/types/core'; import type { AdditionalSidebarTab } from './flowTypes'; import type { FeatureConfig } from '../common/feature-checking'; import type APICache from '../../utils/Cache'; import '../common/fonts.scss'; import '../common/base.scss'; import '../common/modal.scss'; import './ContentSidebar.scss'; type Props = { activitySidebarProps: ActivitySidebarProps, additionalTabs?: Array<AdditionalSidebarTab>, apiHost: string, cache?: APICache, className: string, clientName: string, currentUser?: User, defaultView: string, detailsSidebarProps: DetailsSidebarProps, features: FeatureConfig, fileId?: string, getPreview: Function, getViewer: Function, hasActivityFeed: boolean, hasAdditionalTabs: boolean, hasMetadata: boolean, hasSkills: boolean, hasVersions: boolean, history?: RouterHistory, isDefaultOpen?: boolean, language?: string, messages?: StringMap, metadataSidebarProps: MetadataSidebarProps, onAnnotationSelect?: Function, onVersionChange?: Function, onVersionHistoryClick?: Function, requestInterceptor?: Function, responseInterceptor?: Function, sharedLink?: string, sharedLinkPassword?: string, token: Token, versionsSidebarProps: VersionsSidebarProps, } & ErrorContextProps & WithLoggerProps; type State = { file?: BoxItem, isLoading: boolean, metadataEditors?: Array<MetadataEditor>, }; const MARK_NAME_JS_READY = `${ORIGIN_CONTENT_SIDEBAR}_${EVENT_JS_READY}`; mark(MARK_NAME_JS_READY); class ContentSidebar extends React.Component<Props, State> { props: Props; state: State = { isLoading: true }; api: API; sidebarRef: Sidebar; static defaultProps = { activitySidebarProps: {}, apiHost: DEFAULT_HOSTNAME_API, className: '', clientName: CLIENT_NAME_CONTENT_SIDEBAR, defaultView: '', detailsSidebarProps: {}, getPreview: noop, getViewer: noop, hasActivityFeed: false, hasAdditionalTabs: false, hasMetadata: false, hasSkills: false, isDefaultOpen: true, metadataSidebarProps: {}, }; /** * [constructor] * * @private * @return {ContentSidebar} */ constructor(props: Props) { super(props); const { apiHost, cache, clientName, language, requestInterceptor, responseInterceptor, sharedLink, sharedLinkPassword, token, } = props; this.api = new API({ apiHost, cache, clientName, language, requestInterceptor, responseInterceptor, sharedLink, sharedLinkPassword, token, }); /* eslint-disable react/prop-types */ const { logger } = props; logger.onReadyMetric({ endMarkName: MARK_NAME_JS_READY, }); /* eslint-enable react/prop-types */ } /** * Destroys api instances with caches * * @private * @return {void} */ clearCache(): void { this.api.destroy(true); } /** * Cleanup * * @private * @inheritdoc * @return {void} */ componentWillUnmount() { // Don't destroy the cache while unmounting this.api.destroy(false); } /** * Fetches the file data on load * * @private * @inheritdoc * @return {void} */ componentDidMount() { this.fetchFile(); } /** * Fetches new file data on update * * @private * @inheritdoc * @return {void} */ componentDidUpdate(prevProps: Props): void { const { fileId }: Props = this.props; const { fileId: prevFileId }: Props = prevProps; if (fileId !== prevFileId) { this.fetchFile(); } } /** * Network error callback * * @private * @param {Error} error - Error object * @param {string} code - error code * @return {void} */ errorCallback = (error: ElementsXhrError, code: string): void => { /* eslint-disable no-console */ console.error(error); /* eslint-enable no-console */ /* eslint-disable react/prop-types */ this.props.onError(error, code, { error, }); /* eslint-enable react/prop-types */ }; /** * File fetch success callback that sets the file and view * Only set file if there is data to show in the sidebar. * Skills sidebar doesn't show when there is no data. * * @private * @param {Object} file - Box file * @return {void} */ fetchMetadataSuccessCallback = ({ editors }: { editors: Array<MetadataEditor> }): void => { this.setState({ metadataEditors: editors }); }; /** * Fetches file metadata editors if required * * @private * @return {void} */ fetchMetadata(): void { const { file }: State = this.state; const { metadataSidebarProps }: Props = this.props; const { isFeatureEnabled = true }: MetadataSidebarProps = metadataSidebarProps; // Only fetch metadata if we think that the file may have metadata on it // but currently the metadata feature is turned off. Use case of this would be a free // user who doesn't have the metadata feature but is collabed on a file from a user // who added metadata on the file. If the feature is enabled we always end up showing // the metadata sidebar irrespective of there being any existing metadata or not. const canHaveMetadataSidebar = !isFeatureEnabled && SidebarUtils.canHaveMetadataSidebar(this.props); if (canHaveMetadataSidebar) { this.api .getMetadataAPI(false) .getMetadata(((file: any): BoxItem), this.fetchMetadataSuccessCallback, noop, isFeatureEnabled); } } /** * File fetch success callback that sets the file and sidebar visibility. * Also makes an optional request to fetch metadata editors * * @private * @param {Object} file - Box file * @return {void} */ fetchFileSuccessCallback = (file: BoxItem): void => { this.setState( { file, isLoading: false, }, this.fetchMetadata, ); }; /** * Fetches a file * * @private * @param {Object|void} [fetchOptions] - Fetch options * @return {void} */ fetchFile(fetchOptions: RequestOptions = {}): void { const { fileId }: Props = this.props; this.setState({ isLoading: true }); if (fileId && SidebarUtils.canHaveSidebar(this.props)) { this.api.getFileAPI().getFile(fileId, this.fetchFileSuccessCallback, this.errorCallback, { ...fetchOptions, fields: SIDEBAR_FIELDS_TO_FETCH, }); } } /** * Refreshes the sidebar panel * @returns {void} */ refresh(): void { if (this.sidebarRef) { this.sidebarRef.refresh(); } } /** * Renders the sidebar * * @private * @inheritdoc * @return {Element} */ render() { const { activitySidebarProps, additionalTabs, className, currentUser, defaultView, detailsSidebarProps, fileId, getPreview, getViewer, hasAdditionalTabs, hasActivityFeed, hasMetadata, hasSkills, hasVersions, history, isDefaultOpen, language, messages, metadataSidebarProps, onAnnotationSelect, onVersionChange, onVersionHistoryClick, versionsSidebarProps, }: Props = this.props; const { file, isLoading, metadataEditors }: State = this.state; const initialPath = defaultView.charAt(0) === '/' ? defaultView : `/${defaultView}`; if (!file || !fileId || !SidebarUtils.shouldRenderSidebar(this.props, file, metadataEditors)) { return null; } return ( <Internationalize language={language} messages={messages}> <APIContext.Provider value={(this.api: any)}> <NavRouter history={history} initialEntries={[initialPath]}> <Sidebar activitySidebarProps={activitySidebarProps} additionalTabs={additionalTabs} className={className} currentUser={currentUser} detailsSidebarProps={detailsSidebarProps} file={file} fileId={fileId} getPreview={getPreview} getViewer={getViewer} hasActivityFeed={hasActivityFeed} hasAdditionalTabs={hasAdditionalTabs} hasMetadata={hasMetadata} hasSkills={hasSkills} hasVersions={hasVersions} isDefaultOpen={isDefaultOpen} isLoading={isLoading} metadataEditors={metadataEditors} metadataSidebarProps={metadataSidebarProps} onAnnotationSelect={onAnnotationSelect} onVersionChange={onVersionChange} onVersionHistoryClick={onVersionHistoryClick} versionsSidebarProps={versionsSidebarProps} wrappedComponentRef={ref => { this.sidebarRef = ref; }} /> </NavRouter> </APIContext.Provider> </Internationalize> ); } } export type ContentSidebarProps = Props; export { ContentSidebar as ContentSidebarComponent }; export default flow([ withFeatureProvider, withLogger(ORIGIN_CONTENT_SIDEBAR), withErrorBoundary(ORIGIN_CONTENT_SIDEBAR), ])(ContentSidebar);