box-ui-elements-mlh
Version:
386 lines (352 loc) • 12.1 kB
JavaScript
/**
* @flow
* @file Details sidebar component
* @author Box
*/
import React from 'react';
import flow from 'lodash/flow';
import getProp from 'lodash/get';
import noop from 'lodash/noop';
import { FormattedMessage } from 'react-intl';
import API from '../../api';
import messages from '../common/messages';
import SidebarAccessStats from './SidebarAccessStats';
import SidebarClassification from './SidebarClassification';
import SidebarContent from './SidebarContent';
import SidebarFileProperties from './SidebarFileProperties';
import SidebarNotices from './SidebarNotices';
import SidebarSection from './SidebarSection';
import SidebarVersions from './SidebarVersions';
import { EVENT_JS_READY } from '../common/logger/constants';
import { getBadItemError } from '../../utils/error';
import { mark } from '../../utils/performance';
import { SECTION_TARGETS } from '../common/interactionTargets';
import { SIDEBAR_FIELDS_TO_FETCH } from '../../utils/fields';
import { withAPIContext } from '../common/api-context';
import { withErrorBoundary } from '../common/error-boundary';
import { withLogger } from '../common/logger';
import {
HTTP_STATUS_CODE_FORBIDDEN,
ORIGIN_DETAILS_SIDEBAR,
IS_ERROR_DISPLAYED,
SIDEBAR_VIEW_DETAILS,
} from '../../constants';
import type { ClassificationInfo, FileAccessStats, Errors } from './flowTypes';
import type { WithLoggerProps } from '../../common/types/logging';
import type { ElementsErrorCallback, ErrorContextProps, ElementsXhrError } from '../../common/types/api';
import type { BoxItem } from '../../common/types/core';
import './DetailsSidebar.scss';
type ExternalProps = {
classification?: ClassificationInfo,
elementId: string,
fileId: string,
hasAccessStats?: boolean,
hasClassification?: boolean,
hasNotices?: boolean,
hasProperties?: boolean,
hasRetentionPolicy?: boolean,
hasSidebarInitialized?: boolean,
hasVersions?: boolean,
onAccessStatsClick?: Function,
onClassificationClick?: (e: SyntheticEvent<HTMLButtonElement>) => void,
onRetentionPolicyExtendClick?: Function,
onVersionHistoryClick?: Function,
retentionPolicy?: Object,
} & ErrorContextProps &
WithLoggerProps;
type Props = {
api: API,
} & ExternalProps &
ErrorContextProps &
WithLoggerProps;
type State = {
accessStats?: FileAccessStats,
accessStatsError?: Errors,
file?: BoxItem,
fileError?: Errors,
isLoadingAccessStats: boolean,
};
const MARK_NAME_JS_READY = `${ORIGIN_DETAILS_SIDEBAR}_${EVENT_JS_READY}`;
mark(MARK_NAME_JS_READY);
class DetailsSidebar extends React.PureComponent<Props, State> {
static defaultProps = {
hasNotices: false,
hasProperties: false,
hasAccessStats: false,
hasClassification: false,
hasRetentionPolicy: false,
hasVersions: false,
onError: noop,
};
constructor(props: Props) {
super(props);
this.state = {
isLoadingAccessStats: false,
};
const { logger } = this.props;
logger.onReadyMetric({
endMarkName: MARK_NAME_JS_READY,
});
}
componentDidMount() {
this.fetchFile();
if (this.props.hasAccessStats) {
this.fetchAccessStats();
}
}
componentDidUpdate({ hasAccessStats: prevHasAccessStats }: Props) {
const { hasAccessStats } = this.props;
// Component visibility props such as hasAccessStats can sometimes be flipped after an async call
const hasAccessStatsChanged = prevHasAccessStats !== hasAccessStats;
if (hasAccessStatsChanged) {
if (hasAccessStats) {
this.fetchAccessStats();
} else {
this.setState({
isLoadingAccessStats: false,
accessStats: undefined,
accessStatsError: undefined,
});
}
}
}
/**
* File description update callback
*
* @private
* @param {BoxItem} file - Updated file object
* @return {void}
*/
descriptionChangeSuccessCallback = (file: BoxItem): void => {
this.setState({ file, fileError: undefined });
};
/**
* Fetches a file with the fields needed for details sidebar
*
* @param {Function} successCallback - the success callback
* @param {Function} errorCallback - the error callback
* @return {void}
*/
fetchFile(
successCallback: (file: BoxItem) => void = this.fetchFileSuccessCallback,
errorCallback: ElementsErrorCallback = this.fetchFileErrorCallback,
): void {
const { api, fileId }: Props = this.props;
api.getFileAPI().getFile(fileId, successCallback, errorCallback, {
fields: SIDEBAR_FIELDS_TO_FETCH, // TODO: replace this with DETAILS_SIDEBAR_FIELDS_TO_FETCH as we do not need all the sidebar fields
});
}
/**
* Handles a successful file fetch
*
* @param {Object} file - the box file
* @return {void}
*/
fetchFileSuccessCallback = (file: BoxItem) => {
this.setState({
file,
fileError: undefined,
});
};
/**
* Handles a failed file fetch
*
* @private
* @param {Error} e - API error
* @param {string} code - error code
* @return {void}
*/
fetchFileErrorCallback = (e: ElementsXhrError, code: string) => {
// TODO: handle the error properly (probably with maskError) once files call split out
this.setState({
file: undefined,
});
this.props.onError(e, code, {
e,
});
};
/**
* Handles a failed file description update
*
* @private
* @param {BoxItem} file - Original file object
* @return {void}
*/
descriptionChangeErrorCallback = (file: BoxItem): void => {
// Reset the state back to the original description since the API call failed
this.setState({
file,
fileError: {
inlineError: {
title: messages.fileDescriptionInlineErrorTitleMessage,
content: messages.defaultInlineErrorContentMessage,
},
},
});
};
/**
* Function to update file description
*
* @private
* @param {string} newDescription - New file description
* @return {void}
*/
onDescriptionChange = (newDescription: string): void => {
const { api }: Props = this.props;
const { file }: State = this.state;
if (!file) {
throw getBadItemError();
}
const { description }: BoxItem = file;
if (newDescription === description) {
return;
}
api.getFileAPI().setFileDescription(
file,
newDescription,
this.descriptionChangeSuccessCallback,
this.descriptionChangeErrorCallback,
);
};
/**
* Handles a failed file access stats fetch
*
* @private
* @param {Error} e - API error
* @param {string} code - error code
* @return {void}
*/
fetchAccessStatsErrorCallback = (e: ElementsXhrError, code: string) => {
if (!this.props.hasAccessStats) {
return;
}
const isForbidden = getProp(e, 'status') === HTTP_STATUS_CODE_FORBIDDEN;
let accessStatsError;
if (isForbidden) {
accessStatsError = {
error: messages.fileAccessStatsPermissionsError,
};
} else {
accessStatsError = {
maskError: {
errorHeader: messages.fileAccessStatsErrorHeaderMessage,
errorSubHeader: messages.defaultErrorMaskSubHeaderMessage,
},
};
}
this.setState({
isLoadingAccessStats: false,
accessStats: undefined,
accessStatsError,
});
this.props.onError(e, code, {
e,
[IS_ERROR_DISPLAYED]: !isForbidden,
});
};
/**
* File access stats fetch success callback
*
* @private
* @param {Object} accessStats - access stats for a file
* @return {void}
*/
fetchAccessStatsSuccessCallback = (accessStats: FileAccessStats): void => {
if (!this.props.hasAccessStats) {
return;
}
this.setState({
accessStats,
accessStatsError: undefined,
isLoadingAccessStats: false,
});
};
/**
* Fetches the access stats for a file
*
* @private
* @return {void}
*/
fetchAccessStats(): void {
const { api, fileId }: Props = this.props;
const { isLoadingAccessStats } = this.state;
if (isLoadingAccessStats) {
return;
}
this.setState({ isLoadingAccessStats: true });
api.getFileAccessStatsAPI(false).getFileAccessStats(
fileId,
this.fetchAccessStatsSuccessCallback,
this.fetchAccessStatsErrorCallback,
);
}
refresh(): void {
this.fetchAccessStats();
}
render() {
const {
classification,
elementId,
hasProperties,
hasNotices,
hasAccessStats,
hasClassification,
hasRetentionPolicy,
hasVersions,
onAccessStatsClick,
onVersionHistoryClick,
onClassificationClick,
onRetentionPolicyExtendClick,
retentionPolicy,
}: Props = this.props;
const { accessStats, accessStatsError, file, fileError, isLoadingAccessStats }: State = this.state;
// TODO: Add loading indicator and handle errors once file call is split out
return (
<SidebarContent
className="bcs-details"
elementId={elementId}
sidebarView={SIDEBAR_VIEW_DETAILS}
title={<FormattedMessage {...messages.sidebarDetailsTitle} />}
>
{file && hasNotices && (
<div className="bcs-DetailsSidebar-notices">
<SidebarNotices file={file} />
</div>
)}
{file && hasClassification && (
<SidebarClassification classification={classification} file={file} onEdit={onClassificationClick} />
)}
{file && hasAccessStats && (
<SidebarAccessStats
accessStats={accessStats}
file={file}
onAccessStatsClick={onAccessStatsClick}
{...accessStatsError}
/>
)}
{file && hasProperties && (
<SidebarSection
interactionTarget={SECTION_TARGETS.FILE_PROPERTIES}
title={<FormattedMessage {...messages.sidebarProperties} />}
>
{hasVersions && <SidebarVersions file={file} onVersionHistoryClick={onVersionHistoryClick} />}
<SidebarFileProperties
file={file}
onDescriptionChange={this.onDescriptionChange}
{...fileError}
hasRetentionPolicy={hasRetentionPolicy}
isLoading={isLoadingAccessStats}
onRetentionPolicyExtendClick={onRetentionPolicyExtendClick}
retentionPolicy={retentionPolicy}
/>
</SidebarSection>
)}
</SidebarContent>
);
}
}
export type DetailsSidebarProps = ExternalProps;
export { DetailsSidebar as DetailsSidebarComponent };
export default flow([withLogger(ORIGIN_DETAILS_SIDEBAR), withErrorBoundary(ORIGIN_DETAILS_SIDEBAR), withAPIContext])(
DetailsSidebar,
);