Skip to content
88 changes: 68 additions & 20 deletions src/features/classification/Classification.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// @flow
import * as React from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { Status, Text } from '@box/blueprint-web';
// $FlowFixMe - blueprint-web-assets icons not typed for Flow
import { Shield } from '@box/blueprint-web-assets/icons/Line';

import classNames from 'classnames';
import AsyncLoad from '../../elements/common/async-load';
import ClassifiedBadge from './ClassifiedBadge';
import Label from '../../components/label/Label';
Expand Down Expand Up @@ -33,6 +37,7 @@ type Props = {
controlsFormat?: ControlsFormat,
definition?: string,
isImportedClassification?: boolean,
isRedesignEnabled?: boolean,
isLoadingAppliedBy?: boolean,
isLoadingControls?: boolean,
itemName?: string,
Expand All @@ -54,6 +59,7 @@ const Classification = ({
controlsFormat,
definition,
isImportedClassification = false,
isRedesignEnabled = false,
isLoadingAppliedBy = false,
isLoadingControls,
itemName = '',
Expand Down Expand Up @@ -112,42 +118,83 @@ const Classification = ({
<LoadableAppliedByAiClassificationReason
answer={aiClassificationReason.answer}
citations={aiClassificationReason.citations}
className="bdl-Classification-appliedByAiDetails"
className={classNames({ 'bdl-Classification-appliedByAiDetails': !isRedesignEnabled })}
modifiedAt={aiClassificationReason.modifiedAt}
/>
);
}

return (
return isRedesignEnabled ? (
<Text as="p" data-testid="classification-modifiedby" variant="bodyDefault">
{modifiedByDetails}
</Text>
) : (
<p className="bdl-Classification-modifiedBy" data-testid="classification-modifiedby">
{modifiedByDetails}
</p>
);
};

const Wrapper = isRedesignEnabled ? 'div' : 'article';

return (
<article className={`bdl-Classification ${className}`}>
{isClassified && (
<ClassifiedBadge
color={color}
name={((name: any): string)}
onClick={onClick}
tooltipText={isTooltipMessageEnabled ? definition : undefined}
/>
)}
{isInlineMessageEnabled && (
<Label text={<FormattedMessage {...messages.definition} />}>
<p className="bdl-Classification-definition">{definition}</p>
</Label>
)}
<Wrapper
className={classNames('bdl-Classification', className, {
'bdl-Classification--redesign': isRedesignEnabled,
})}
>
{isClassified &&
(isRedesignEnabled ? (
<Status color={color} icon={Shield} iconPosition="left" text={name} />
) : (
<ClassifiedBadge
color={color}
name={((name: any): string)}
onClick={onClick}
tooltipText={isTooltipMessageEnabled ? definition : undefined}
/>
))}
{isInlineMessageEnabled &&
(isRedesignEnabled ? (
<div className="bdl-Classification-propertySection">
<Text
as="p"
className="bdl-Classification-sectionLabel"
color="textOnLightSecondary"
variant="bodyDefaultSemibold"
>
<FormattedMessage {...messages.definition} />
</Text>
<Text as="p" variant="bodyDefault">
{definition}
</Text>
</div>
) : (
<Label text={<FormattedMessage {...messages.definition} />}>
<p className="bdl-Classification-definition">{definition}</p>
</Label>
))}
{isNotClassifiedMessageVisible && (
<span className="bdl-Classification-missingMessage">
<FormattedMessage {...messages.missing} />
</span>
)}
{shouldRenderModificationDetails && (
<Label text={<FormattedMessage {...modificationTitleLabel} />}>{renderModificationDetails()}</Label>
)}
{shouldRenderModificationDetails &&
(isRedesignEnabled ? (
<div className="bdl-Classification-propertySection">
<Text
as="p"
className="bdl-Classification-sectionLabel"
color="textOnLightSecondary"
variant="bodyDefaultSemibold"
>
<FormattedMessage {...modificationTitleLabel} />
</Text>
<div className="bdl-Classification-propertyContent">{renderModificationDetails()}</div>
</div>
) : (
<Label text={<FormattedMessage {...modificationTitleLabel} />}>{renderModificationDetails()}</Label>
))}

{isSecurityControlsEnabled && (
<SecurityControls
Expand All @@ -156,14 +203,15 @@ const Classification = ({
controls={controls}
controlsFormat={controlsFormat}
definition={definition}
isRedesignEnabled={isRedesignEnabled}
itemName={itemName}
maxAppCount={maxAppCount}
shouldRenderLabel
shouldDisplayAppsAsIntegrations={shouldDisplayAppsAsIntegrations}
/>
)}
{isControlsIndicatorEnabled && <LoadingIndicator />}
</article>
</Wrapper>
);
};

Expand Down
13 changes: 13 additions & 0 deletions src/features/classification/Classification.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
}
}

.bdl-Classification--redesign {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-4);
}

.bdl-Classification-definition {
max-height: $bdl-line-height * 5;
margin-top: 3px;
Expand All @@ -23,3 +30,9 @@
margin-bottom: var(--space-5);
margin-top: var(--space-05);
}

.bdl-Classification-propertySection {
.bdl-Classification-sectionLabel {
margin-block-end: var(--space-1);
}
}
85 changes: 85 additions & 0 deletions src/features/classification/Classification.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as React from 'react';

import Classification, { STYLE_INLINE } from './Classification';
import { SECURITY_CONTROLS_FORMAT } from './constants';

const definition =
'[Prohibits external access] Internal data is not accessible by external users and can only be accessed by Box employees and contractors. Examples: New hire documents, policies, procedures, announcements, and Boxer training.';

const controls = {
boxSignRequest: { enabled: true },
download: { desktop: { restrictManagedUsers: 'ownersCoOwners' } },
sharedLink: { accessLevel: 'collabOnly' },
};

const baseProps = {
color: '#ffeb7f',
definition,
messageStyle: STYLE_INLINE,
name: 'BOX-ONLY',
modifiedAt: '2025-01-15T10:30:00Z',
modifiedBy: 'Robert Robertson',
};

export const Legacy = () => <Classification {...baseProps} />;

export const Redesigned = () => <Classification {...baseProps} isRedesignEnabled />;

export const RedesignedWithControls = () => (
<Classification
{...baseProps}
controls={controls}
controlsFormat={SECURITY_CONTROLS_FORMAT.SHORT_WITH_BTN}
isRedesignEnabled
itemName="welcome.pdf"
/>
);

const aiClassificationReason = {
answer: 'This document contains internal policies, onboarding procedures, and employee training materials that are restricted to Box employees and contractors only.',
modifiedAt: '2025-01-15T10:30:00Z',
citations: [
{ content: 'New hire onboarding policy v3', fileId: '123', location: 'Page 1', title: 'Onboarding Policy.pdf' },
{ content: 'Internal training schedule 2025', fileId: '456', location: 'Page 2', title: 'Training Guide.docx' },
],
};

export const RedesignedWithAiClassification = () => (
<Classification
{...baseProps}
aiClassificationReason={aiClassificationReason}
isRedesignEnabled
shouldUseAppliedByLabels
/>
);

export const RedesignedFull = () => (
<Classification
{...baseProps}
aiClassificationReason={aiClassificationReason}
controls={controls}
controlsFormat={SECURITY_CONTROLS_FORMAT.SHORT_WITH_BTN}
isRedesignEnabled
itemName="welcome.pdf"
shouldUseAppliedByLabels
/>
);

export const LegacyWithAiClassification = () => (
<Classification {...baseProps} aiClassificationReason={aiClassificationReason} shouldUseAppliedByLabels />
);

export const LegacyWithControls = () => (
<Classification
{...baseProps}
controls={controls}
controlsFormat={SECURITY_CONTROLS_FORMAT.SHORT_WITH_BTN}
itemName="welcome.pdf"
/>
);

export default {
title: 'Features/Classification',
component: Classification,
decorators: [(story: () => React.ReactNode) => <div style={{ width: 260 }}>{story()}</div>],
};
72 changes: 51 additions & 21 deletions src/features/classification/security-controls/SecurityControls.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Text, TextButton } from '@box/blueprint-web';

import { DEFAULT_MAX_APP_COUNT, SECURITY_CONTROLS_FORMAT } from '../constants';
import { getShortSecurityControlsMessage, getFullSecurityControlsMessages } from './utils';
Expand All @@ -21,6 +22,7 @@ type Props = {
controls: Controls,
controlsFormat: ControlsFormat,
definition?: string,
isRedesignEnabled?: boolean,
itemName?: string,
maxAppCount?: number,
shouldRenderLabel?: boolean,
Expand All @@ -35,6 +37,7 @@ class SecurityControls extends React.Component<Props, State> {
static defaultProps = {
classificationName: '',
definition: '',
isRedesignEnabled: false,
itemName: '',
controls: {},
controlsFormat: SHORT,
Expand All @@ -58,6 +61,7 @@ class SecurityControls extends React.Component<Props, State> {
controls,
controlsFormat,
definition,
isRedesignEnabled,
itemName,
maxAppCount,
shouldRenderLabel,
Expand All @@ -81,41 +85,67 @@ class SecurityControls extends React.Component<Props, State> {
return null;
}

const { isSecurityControlsModalOpen } = this.state;
const shouldShowSecurityControlsModal =
controlsFormat === SHORT_WITH_BTN && !!itemName && !!classificationName && !!definition;

let itemsList = (
<ul className="bdl-SecurityControls">
{items.map(({ message, tooltipMessage }, index) => (
<SecurityControlsItem key={index} message={message} tooltipMessage={tooltipMessage} />
<SecurityControlsItem
key={index}
isRedesignEnabled={isRedesignEnabled}
message={message}
tooltipMessage={tooltipMessage}
/>
))}
</ul>
);

const { isSecurityControlsModalOpen } = this.state;
const shouldShowSecurityControlsModal =
controlsFormat === SHORT_WITH_BTN && !!itemName && !!classificationName && !!definition;

const securityControlsModal = shouldShowSecurityControlsModal && (
<SecurityControlsModal
classificationColor={classificationColor}
classificationName={classificationName}
closeModal={this.closeModal}
definition={definition}
itemName={itemName}
isSecurityControlsModalOpen={isSecurityControlsModalOpen}
modalItems={modalItems}
/>
);

if (shouldRenderLabel) {
itemsList = <Label text={<FormattedMessage {...messages.securityControlsLabel} />}>{itemsList}</Label>;
itemsList = isRedesignEnabled ? (
<div className="bdl-Classification-propertySection">
<Text
as="p"
className="bdl-Classification-sectionLabel"
color="textOnLightSecondary"
variant="bodyDefaultSemibold"
>
<FormattedMessage {...messages.securityControlsLabel} />
</Text>
{itemsList}
{shouldShowSecurityControlsModal && (
<TextButton className="bdl-SecurityControls-viewAllButton" onClick={this.openModal}>
<FormattedMessage {...messages.viewAll} />
</TextButton>
)}
</div>
) : (
<Label text={<FormattedMessage {...messages.securityControlsLabel} />}>{itemsList}</Label>
);
}

return (
<>
{itemsList}
{shouldShowSecurityControlsModal && (
<>
<PlainButton className="lnk" onClick={this.openModal} type="button">
<FormattedMessage {...messages.viewAll} />
</PlainButton>
<SecurityControlsModal
classificationColor={classificationColor}
classificationName={classificationName}
closeModal={this.closeModal}
definition={definition}
itemName={itemName}
isSecurityControlsModalOpen={isSecurityControlsModalOpen}
modalItems={modalItems}
/>
</>
{!isRedesignEnabled && shouldShowSecurityControlsModal && (
<PlainButton className="lnk" onClick={this.openModal} type="button">
<FormattedMessage {...messages.viewAll} />
</PlainButton>
)}
{securityControlsModal}
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@
margin: 0;
list-style: none;
}

.bdl-SecurityControls-viewAllButton {
padding: 0;
margin-block-start: var(--space-1);
}
Loading