Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/elements/content-sidebar/versions/VersionsItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const VersionsItem = ({
onClick={handleAction(onPreview)}
>
<div className="bcs-VersionsItem-badge">
<VersionsItemBadge versionNumber={versionNumber} />
<VersionsItemBadge isCurrent={isCurrent} versionNumber={versionNumber} />
</div>

<div className="bcs-VersionsItem-details">
Expand Down
6 changes: 6 additions & 0 deletions src/elements/content-sidebar/versions/VersionsItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
box-shadow: $bdl-btn-primary-box-shadow;
}
}

.bcs-VersionsItemActions-toggle--modernized {
position: absolute;
top: var(--space-2);
right: var(--space-2);
}
}

.bcs-VersionsItem-badge {
Expand Down
47 changes: 33 additions & 14 deletions src/elements/content-sidebar/versions/VersionsItemActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
*/

import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { IconButton } from '@box/blueprint-web';
import { Ellipsis } from '@box/blueprint-web-assets/icons/Fill';
import { useFeatureConfig } from '../../common/feature-checking/hooks';
import DropdownMenu from '../../../components/dropdown-menu';
import IconClockPast from '../../../icons/general/IconClockPast';
import IconDownload from '../../../icons/general/IconDownload';
Expand Down Expand Up @@ -59,6 +62,9 @@ const VersionsItemActions = ({
showPromote = false,
showRestore = false,
}: Props) => {
const { enabled: isPreviewModernizationEnabled } = useFeatureConfig('previewModernization');
const { formatMessage } = useIntl();

if (!showDelete && !showDownload && !showPreview && !showPromote && !showRestore) {
return null;
}
Expand All @@ -71,19 +77,32 @@ const VersionsItemActions = ({
isRightAligned
onMenuClose={handleMenuClose}
>
<PlainButton
className="bcs-VersionsItemActions-toggle"
data-resin-iscurrent={isCurrent}
data-resin-itemid={fileId}
data-resin-target="overflow"
onClick={handleToggleClick}
type="button"
>
<IconEllipsis height={4} width={14} />
<FormattedMessage {...messages.versionActionToggle}>
{(text: string) => <span className="accessibility-hidden">{text}</span>}
</FormattedMessage>
</PlainButton>
{isPreviewModernizationEnabled ? (
<IconButton
aria-label={formatMessage(messages.versionActionToggle)}
className="bcs-VersionsItemActions-toggle--modernized"
data-resin-iscurrent={isCurrent}
data-resin-itemid={fileId}
data-resin-target="overflow"
icon={Ellipsis}
size="small"
onClick={handleToggleClick}
/>
) : (
<PlainButton
className="bcs-VersionsItemActions-toggle"
data-resin-iscurrent={isCurrent}
data-resin-itemid={fileId}
data-resin-target="overflow"
onClick={handleToggleClick}
type="button"
>
<IconEllipsis height={4} width={14} />
<FormattedMessage {...messages.versionActionToggle}>
{(text: string) => <span className="accessibility-hidden">{text}</span>}
</FormattedMessage>
</PlainButton>
)}

<Menu
className="bcs-VersionsItemActions-menu"
Expand Down
13 changes: 10 additions & 3 deletions src/elements/content-sidebar/versions/VersionsItemBadge.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
*/
import * as React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl';
import classNames from 'classnames';
import messages from './messages';
import './VersionsItemBadge.scss';

type Props = {
intl: any,
versionNumber: string,
isCurrent?: boolean,
versionNumber?: string,
};

const VersionsItemBadge = ({ intl, versionNumber }: Props) => {
const VersionsItemBadge = ({ intl, isCurrent, versionNumber }: Props) => {
const intlValues = { versionNumber };

return (
<div aria-label={intl.formatMessage(messages.versionNumberLabel, intlValues)} className="bcs-VersionsItemBadge">
<div
aria-label={intl.formatMessage(messages.versionNumberLabel, intlValues)}
className={classNames('bcs-VersionsItemBadge', {
'bcs-VersionsItemBadge--current': isCurrent,
})}
>
<FormattedMessage {...messages.versionNumberBadge} values={intlValues} />
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion src/elements/content-sidebar/versions/VersionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import * as React from 'react';
import { Route } from 'react-router-dom';
import classNames from 'classnames';
import VersionsItem from './VersionsItem';
import { useFeatureConfig } from '../../common/feature-checking/hooks';
import type { BoxItemVersion } from '../../../common/types/core';
import type { InternalSidebarNavigation } from '../../common/types/SidebarNavigation';
import './VersionsList.scss';
Expand All @@ -22,6 +24,8 @@ type Props = {
};

const VersionsList = ({ currentId, internalSidebarNavigation, routerDisabled = false, versions, ...rest }: Props) => {
const { enabled: isPreviewModernizationEnabled } = useFeatureConfig('previewModernization');

const renderVersionItemWithoutRouter = (version: BoxItemVersion) => (
<VersionsItem
isCurrent={currentId === version.id}
Expand All @@ -45,7 +49,11 @@ const VersionsList = ({ currentId, internalSidebarNavigation, routerDisabled = f
);

return (
<ul className="bcs-VersionsList">
<ul
className={classNames('bcs-VersionsList', {
'bcs-VersionsList--modernized': isPreviewModernizationEnabled,
})}
>
{versions.map(version => (
<li className="bcs-VersionsList-item" key={version.id}>
{routerDisabled ? renderVersionItemWithoutRouter(version) : renderVersionItemWithRouter(version)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,134 @@
import * as React from 'react';
import { shallow } from 'enzyme/build';
import IconClockPast from '../../../../icons/general/IconClockPast';
import IconDownload from '../../../../icons/general/IconDownload';
import IconEllipsis from '../../../../icons/general/IconEllipsis';
import IconOpenWith from '../../../../icons/general/IconOpenWith';
import IconTrash from '../../../../icons/general/IconTrash';
import IconUpload from '../../../../icons/general/IconUpload';
import Tooltip from '../../../../components/tooltip/Tooltip';
import { userEvent } from '@testing-library/user-event';
import { screen, render } from '../../../../test-utils/testing-library';
import VersionsItemActions from '../VersionsItemActions';

describe('elements/content-sidebar/versions/VersionsItemActions', () => {
const getWrapper = (props = {}) => shallow(<VersionsItemActions isDownloadable isPreviewable {...props} />);
const defaultProps = {
fileId: '12345',
};

const renderComponent = (props = {}, features = {}) =>
render(<VersionsItemActions {...defaultProps} {...props} />, {
wrapperProps: { features },
});

describe('render', () => {
test.each([true, false])('should return the correct menu items based on options', option => {
const wrapper = getWrapper({
showDelete: option,
showDownload: option,
showPreview: option,
showPromote: option,
showRestore: option,
test('should return null when no actions are shown', () => {
const { container } = renderComponent({
showDelete: false,
showDownload: false,
showPreview: false,
showPromote: false,
showRestore: false,
});

expect(container).toBeEmptyDOMElement();
});

test('should render actions toggle button when at least one action is shown', () => {
renderComponent({ showDownload: true });

expect(screen.getByRole('button', { name: 'Toggle Actions Menu' })).toBeInTheDocument();
});

test.each([
{ actionProp: 'showPreview', label: 'Preview' },
{ actionProp: 'showDownload', label: 'Download' },
{ actionProp: 'showPromote', label: 'Make Current' },
{ actionProp: 'showRestore', label: 'Restore' },
{ actionProp: 'showDelete', label: 'Delete' },
])('should render $label action when $actionProp is true', async ({ actionProp, label }) => {
renderComponent({ [actionProp]: true });

const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
await userEvent.click(toggleButton);

expect(screen.getByRole('menuitem', { name: new RegExp(label) })).toBeInTheDocument();
});

test('should render all actions when all show props are true', async () => {
renderComponent({
showDelete: true,
showDownload: true,
showPreview: true,
showPromote: true,
showRestore: true,
});

expect(wrapper.find(IconEllipsis).exists()).toBe(option); // Versions show actions if any permission is true
expect(wrapper.find(IconClockPast).exists()).toBe(option);
expect(wrapper.find(IconDownload).exists()).toBe(option);
expect(wrapper.find(IconOpenWith).exists()).toBe(option);
expect(wrapper.find(IconTrash).exists()).toBe(option);
expect(wrapper.find(IconUpload).exists()).toBe(option);
expect(wrapper).toMatchSnapshot();
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
await userEvent.click(toggleButton);

expect(screen.getByRole('menuitem', { name: /Preview/ })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Download/ })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Make Current/ })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Restore/ })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Delete/ })).toBeInTheDocument();
});

test.each([true, false])('should enable/disable actions and tooltips if isRetained is %s', option => {
const wrapper = getWrapper({
isRetained: option,
test('should disable delete action when isRetained is true', async () => {
renderComponent({
isRetained: true,
showDelete: true,
});

expect(wrapper.find(Tooltip).prop('isDisabled')).toBe(!option);
expect(wrapper).toMatchSnapshot();
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
await userEvent.click(toggleButton);

const deleteButton = screen.getByRole('menuitem', { name: /Delete/ });
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
});

test('should not disable delete action when isRetained is false', async () => {
renderComponent({
isRetained: false,
showDelete: true,
});

const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
await userEvent.click(toggleButton);

const deleteButton = screen.getByRole('menuitem', { name: /Delete/ });
expect(deleteButton).not.toHaveAttribute('aria-disabled', 'true');
});

describe('with previewModernization feature enabled', () => {
const previewModernizationFeatures = {
previewModernization: { enabled: true },
};

test('should render modernized toggle button', () => {
renderComponent({ showDownload: true }, previewModernizationFeatures);

const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
expect(toggleButton).toHaveClass('bcs-VersionsItemActions-toggle--modernized');
});

test('should render actions in dropdown menu', async () => {
renderComponent(
{
showDownload: true,
showPreview: true,
},
previewModernizationFeatures,
);

const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
await userEvent.click(toggleButton);

expect(screen.getByRole('menuitem', { name: /Download/ })).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Preview/ })).toBeInTheDocument();
});
});

describe('with previewModernization feature disabled', () => {
test('should render legacy toggle button', () => {
renderComponent({ showDownload: true });

const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
expect(toggleButton).toHaveClass('bcs-VersionsItemActions-toggle');
expect(toggleButton).not.toHaveClass('bcs-VersionsItemActions-toggle--modernized');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
import * as React from 'react';
import { render } from 'enzyme/build';
import { screen, render } from '../../../../test-utils/testing-library';
import VersionsItemBadge from '../VersionsItemBadge';

describe('elements/content-sidebar/versions/VersionsItemBadge', () => {
const getWrapper = (props = {}) => render(<VersionsItemBadge {...props} />);
const renderComponent = (props = {}) => render(<VersionsItemBadge {...props} />);

describe('render', () => {
test('should match its snapshot', () => {
const wrapper = getWrapper({ versionNumber: '1' });
expect(wrapper).toMatchSnapshot();
test('should render version number badge', () => {
renderComponent({ versionNumber: '1' });

expect(screen.getByText('V1')).toBeInTheDocument();
});

test('should have correct aria-label', () => {
renderComponent({ versionNumber: '5' });

expect(screen.getByLabelText('Version number 5')).toBeInTheDocument();
});

test.each`
isCurrent | shouldHaveCurrentClass
${true} | ${true}
${false} | ${false}
${undefined} | ${false}
`(
'should apply current class correctly when isCurrent is $isCurrent',
({ isCurrent, shouldHaveCurrentClass }) => {
renderComponent({ versionNumber: '1', isCurrent });

const badge = screen.getByText('V1');
expect(badge).toHaveClass('bcs-VersionsItemBadge');
if (shouldHaveCurrentClass) {
expect(badge).toHaveClass('bcs-VersionsItemBadge--current');
} else {
expect(badge).not.toHaveClass('bcs-VersionsItemBadge--current');
}
},
);
});
});
Loading