Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(docgen-sidebar): add collapsible component to hide nested tags #3626

Merged
merged 15 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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/components/loading-indicator/LoadingIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface LoadingIndicatorProps {
}

const LoadingIndicator = ({ className = '', size = LoadingIndicatorSize.DEFAULT }: LoadingIndicatorProps) => (
<div className={`crawler ${className} is-${size}`}>
<div className={`crawler ${className} is-${size}`} data-testid="loading-indicator">
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
<div />
<div />
<div />
Expand Down
61 changes: 44 additions & 17 deletions src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
@use '@box/blueprint-web-assets/tokens/tokens.scss';

@import '../../common/variables';

.bcs-DocGenSidebar {
padding: 0 24px 0 24px;
padding: 0 tokens.$space-6 0 tokens.$space-6;
}

.bcs-DocGen--center {
Expand All @@ -10,26 +12,51 @@
}

.bcs-TagsSection {
margin-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid $bdl-gray-10;
margin-top: tokens.$space-6;
padding-bottom: tokens.$space-6;
border-bottom: tokens.$border-1 solid tokens.$gray-10;
}

.bcs-TagsSection-header {
font-size: $bdl-fontSize-minimum;
line-height: 14px;
font-size: tokens.$caption-default-font-size;
line-height: tokens.$caption-default-line-height;
}

.bcs-DocGen-accordion {
padding: 0;

&:first-child {
padding: 0;
}
}

.bcs-TagsSection-accordion-wrapper {
width: '100%';
min-width: 'fit-content';
height: '100%';
overflow-x: 'auto';
overflow-y: 'auto';
}

.bcs-DocGen-accordion * {
width: auto;
padding: 0;
font-size: tokens.$body-default-bold-font-size;

.bcs-DocGen-tagPath {
padding-left: 0;
font-weight: bold;
}
}

.bcs-DocGen-tagPath {
display: block;
height: 32px;
padding: 6px 12px;
font-weight: bold;
font-size: $bdl-fontSize--dejaBlue;
.bcs-DocGen-collapsible {
padding-top: tokens.$space-3;
padding-right: tokens.$space-6;
border: none;
}

.bcs-DocGenSidebar-loading {
margin-top: 16px;
margin-top: tokens.$space-4;
}

.bcs-DocGen-emptyState {
Expand All @@ -39,8 +66,8 @@
}

.bcs-DocGen-emptyState--icon {
margin-top: 60px;
margin-bottom: 15px;
margin-top: tokens.$space-15;
margin-bottom: tokens.$space-4;
}

.bcs-DocGen-error-state {
Expand All @@ -49,7 +76,7 @@
align-items: center;

&--icon {
margin-top: 60px;
margin-bottom: 15px;
margin-top: tokens.$space-15;
margin-bottom: tokens.$space-4;
}
}
12 changes: 10 additions & 2 deletions src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import classNames from 'classnames';
import flow from 'lodash/flow';
import { injectIntl, IntlShape } from 'react-intl';

import { LoadingIndicator } from '@box/blueprint-web';

// @ts-ignore: no ts definition
// eslint-disable-next-line import/named
import { ORIGIN_DOCGEN_SIDEBAR, SIDEBAR_VIEW_DOCGEN } from '../../../constants';
Expand All @@ -12,7 +14,6 @@ import { withAPIContext } from '../../common/api-context';
// @ts-ignore: no ts definition
// eslint-disable-next-line import/named
import { withErrorBoundary } from '../../common/error-boundary';
import LoadingIndicator from '../../../components/loading-indicator';
// @ts-ignore: no ts definition
// eslint-disable-next-line import/named
import { withLogger } from '../../common/logger';
Expand Down Expand Up @@ -127,7 +128,14 @@ const DocGenSidebar = ({ intl, getDocGenTags }: Props) => {
<SidebarContent sidebarView={SIDEBAR_VIEW_DOCGEN} title={intl.formatMessage(messages.docGenTags)}>
<div className={classNames('bcs-DocGenSidebar', { center: isEmpty || hasError || isLoading })}>
{hasError && <Error onClick={loadTags} />}
{isLoading && <LoadingIndicator className="bcs-DocGenSidebar-loading" />}
{isLoading && (
<LoadingIndicator
aria-label="Loading..."
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
aria-live="polite"
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
variant="default"
className="bcs-DocGenSidebar-loading"
/>
)}
{!hasError && !isLoading && isEmpty && <EmptyTags />}
{!hasError && !isLoading && !isEmpty && (
<>
Expand Down
2 changes: 1 addition & 1 deletion src/elements/content-sidebar/DocGenSidebar/Error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {
};

const Error = ({ onClick }: Props) => (
<div className="bcs-DocGen-error-state">
<div className="bcs-DocGen-error-state" data-testid="docgen-sidebar-error">
<RefreshIcon className="bcs-DocGen-error-state--icon" />
<div>
<FormattedMessage {...messages.errorCouldNotLoad} />
Expand Down
28 changes: 20 additions & 8 deletions src/elements/content-sidebar/DocGenSidebar/TagTree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as React from 'react';
import isEmpty from 'lodash/isEmpty';
import { Accordion } from '@box/blueprint-web';

import './DocGenSidebar.scss';
import { JsonPathsMap } from './types';

Expand All @@ -13,16 +16,25 @@ const TagTree = ({ data, level = 0 }: TagTreeProps) => {
}

return (
<div>
<Accordion
type='multiple'
className="bcs-DocGen-accordion"
>
{Object.keys(data)
.sort()
.map(key => (
<div key={`${key}-${level}`} style={{ paddingLeft: `${level * 12}px` }}>
<span className="bcs-DocGen-tagPath">{key}</span>
{data[key] && <TagTree data={data[key]} level={level + 1} />}
</div>
))}
</div>
.map(key => {
if (isEmpty(data[key])) {
return(
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
<Accordion.Item value={key} key={`${key}-${level}`} style={{ paddingLeft: `${level * 12}px` }} fixed className="bcs-DocGen-collapsible">
<span className="bcs-DocGen-tagPath">{key}</span>
</Accordion.Item>
)
}
return (<Accordion.Item value={key} title={key} key={`${key}-${level}`} style={{ paddingLeft: `${level * 12}px` }} className="bcs-DocGen-collapsible">
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
{data[key] && <TagTree data={data[key]} level={level + 1} />}
</Accordion.Item>)
})}
</Accordion>
);
};

Expand Down
7 changes: 4 additions & 3 deletions src/elements/content-sidebar/DocGenSidebar/TagsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ const TagsSection = ({ data, message }: Props) => {
}

return (
<div className="bcs-TagsSection">
<div className="bcs-TagsSection" data-testid="bcs-TagsSection">
<span className="bcs-TagsSection-header">
<FormattedMessage {...message} />
</span>

<TagTree data={data} />
<div className="bcs-TagsSection-accordion-wrapper">
<TagTree data={data} />
</div>
</div>
);
};
Expand Down
114 changes: 37 additions & 77 deletions src/elements/content-sidebar/__tests__/DocgenSidebar.test.tsx
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
rustam-e marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import React, { act } from 'react';
import { mount } from 'enzyme';
import { MessageDescriptor, FormattedMessage } from 'react-intl';
import React from 'react';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import { MessageDescriptor } from 'react-intl';
import { render } from '../../../test-utils/testing-library';

rustam-e marked this conversation as resolved.
Show resolved Hide resolved
import { DocGenSidebarComponent as DocGenSidebar } from '../DocGenSidebar/DocGenSidebar';
import LoadingIndicator from '../../../components/loading-indicator';
import Error from '../DocGenSidebar/Error';
import mockData from '../__mocks__/DocGenSidebar';
import mockData from '../__mocks__/DocGenSidebar.mock';

const intl = {
formatMessage: (message: MessageDescriptor) => message.defaultMessage,
Expand All @@ -30,101 +28,63 @@ const defaultProps = {
...docGenSidebarProps,
};

// TODO renable when development returns to normal
xdescribe('elements/content-sidebar/DocGenSidebar', () => {
const getWrapper = (props = defaultProps, options = {}) =>
mount(<DocGenSidebar logger={{ onReadyMetric: jest.fn() }} {...props} />, options);
describe('elements/content-sidebar/DocGenSidebar', () => {
const renderComponent = (props = defaultProps) =>
render(<DocGenSidebar logger={{ onReadyMetric: jest.fn() }} {...props} />);

test('componentDidMount() should call fetch tags', async () => {
await act(async () => {
await getWrapper(defaultProps);
});
expect(docGenSidebarProps.getDocGenTags).toHaveBeenCalled();
renderComponent();
await waitFor(() => expect(docGenSidebarProps.getDocGenTags).toHaveBeenCalled());
});

test('should render DocGen sidebar component correctly with tags list', async () => {
let wrapper;
await act(async () => {
wrapper = getWrapper(defaultProps);
});
wrapper!.update();
const tagList = wrapper!.find('span.bcs-DocGen-tagPath');
expect(tagList).toHaveLength(26);
expect(wrapper).toMatchSnapshot();
renderComponent();
const tagList = await screen.findAllByTestId('bcs-TagsSection');
expect(tagList).toHaveLength(2);
});

test('should render empty state when there are no tags', async () => {
let wrapper;
await act(async () => {
wrapper = await getWrapper({
...defaultProps,
getDocGenTags: noTagsMock,
});
renderComponent({
...defaultProps,
getDocGenTags: noTagsMock,
});

wrapper!.update();
const emptyState = wrapper!.find(FormattedMessage).at(0);
expect(emptyState.prop('defaultMessage')).toEqual('This document has no tags');
expect(emptyState).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
const emptyState = await screen.findByText('This document has no tags');
expect(emptyState).toBeInTheDocument();
});

test('should render loading state', async () => {
const wrapper = await getWrapper({
renderComponent({
...defaultProps,
getDocGenTags: noTagsMock,
});
const loadingState = wrapper!.find(LoadingIndicator);
expect(loadingState).toHaveLength(1);
expect(wrapper).toMatchSnapshot();

const loadingState = await screen.getByRole('status'); // Assuming LoadingIndicator has a test id
expect(loadingState).toBeInTheDocument();
});
rustam-e marked this conversation as resolved.
Show resolved Hide resolved

test('should re-trigger getDocGenTags on click on refresh button', async () => {
let wrapper;
await act(async () => {
wrapper = await getWrapper({
...defaultProps,
getDocGenTags: errorTagsMock,
});
renderComponent({
...defaultProps,
getDocGenTags: errorTagsMock,
});
wrapper!.update();

const refreshBtn = wrapper!.find('button');
refreshBtn.simulate('click');
wrapper!.update();
expect(errorTagsMock).toBeCalledTimes(2);
});
const errorState = await screen.findByTestId('docgen-sidebar-error');
expect(errorState).toBeInTheDocument();

test('should render error state', async () => {
let wrapper;
await act(async () => {
wrapper = await getWrapper({
...defaultProps,
getDocGenTags: errorTagsMock,
});
});
wrapper!.update();
const refreshButton = screen.getByRole('button');
fireEvent.click(refreshButton);
rustam-e marked this conversation as resolved.
Show resolved Hide resolved

const loadingState = wrapper!.find(Error);
expect(loadingState).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
const refreshBtn = wrapper!.find('button');
expect(refreshBtn).toHaveLength(1);
await waitFor(() => expect(errorTagsMock).toBeCalledTimes(2));
});

test('should handle undefined data ', async () => {
let wrapper;
await act(async () => {
wrapper = await getWrapper({
...defaultProps,
getDocGenTags: noDataMock,
});
test('should handle undefined data', async () => {
renderComponent({
...defaultProps,
getDocGenTags: noDataMock,
});

wrapper!.update();
const emptyState = wrapper!.find(FormattedMessage).at(0);
expect(emptyState.prop('defaultMessage')).toEqual("We couldn't load the tags");
expect(emptyState).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
const emptyState = await screen.findByText("We couldn't load the tags");
expect(emptyState).toBeInTheDocument();
});
});
Loading
Loading