Skip to content

Commit

Permalink
feat(metadata-sidebar): initialize FilterInstancesDropdown from histo…
Browse files Browse the repository at this point in the history
…ry (#3775)

* feat(metadata-sidebar): initialize FilterInstancesDropdown from history

* feat(metadata-sidebar): initialize FilterInstancesDropdown from history

* feat(metadata-sidebar): initialize FilterInstancesDropdown from history

* feat(metadata-sidebar): initialize FilterInstancesDropdown from history

* feat(metadata-sidebar): set FilterInstancesDropdown

* feat(metadata-sidebar): set FilterInstancesDropdown

* feat(metadata-sidebar): set FilterInstancesDropdown

* feat(metadata-sidebar): initialize FilterInstancesDropdown from history

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
wpiesiak and mergify[bot] authored Dec 17, 2024
1 parent d5bdd04 commit 828b527
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 33 deletions.
21 changes: 14 additions & 7 deletions src/elements/content-preview/PreviewNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import IconNavigateRight from '../../icons/general/IconNavigateRight';
import PlainButton from '../../components/plain-button/PlainButton';
import messages from '../common/messages';
import type { BoxItem } from '../../common/types/core';
import { SIDEBAR_VIEW_METADATA } from '../../constants';

type Props = {
collection: Array<string | BoxItem>,
Expand All @@ -30,17 +31,25 @@ const PreviewNavigation = ({ collection = [], currentIndex, intl, onNavigateLeft
return null;
}

const goToActiveSidebarTab = (routeParams, history) => {
if (routeParams.deeplink) {
if (routeParams.activeTab === SIDEBAR_VIEW_METADATA) {
history.push(`/${routeParams.activeTab}/${routeParams.deeplink}/${routeParams[0]}`);
} else {
history.push(`/${routeParams.activeTab}`);
}
}
};

return (
<Route path={['/:activeTab/:deeplink', '/']}>
<Route path={['/:activeTab/:deeplink/*', '/']}>
{({ match, history }) => (
<>
{hasLeftNavigation && (
<PlainButton
className="bcpr-navigate-left"
onClick={() => {
if (match.params.deeplink) {
history.push(`/${match.params.activeTab}`);
}
goToActiveSidebarTab(match.params, history);
onNavigateLeft();
}}
title={intl.formatMessage(messages.previousFile)}
Expand All @@ -53,9 +62,7 @@ const PreviewNavigation = ({ collection = [], currentIndex, intl, onNavigateLeft
<PlainButton
className="bcpr-navigate-right"
onClick={() => {
if (match.params.deeplink) {
history.push(`/${match.params.activeTab}`);
}
goToActiveSidebarTab(match.params, history);
onNavigateRight();
}}
title={intl.formatMessage(messages.nextFile)}
Expand Down
61 changes: 53 additions & 8 deletions src/elements/content-preview/__tests__/PreviewNavigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@ import noop from 'lodash/noop';
import { mount } from 'enzyme';
import { PreviewNavigationComponent as PreviewNavigation } from '../PreviewNavigation';

const historyMock = {
const historyMockDefault = {
location: { pathname: '/activity/tasks/1234', hash: '' },
listen: jest.fn(),
push: jest.fn(),
entries: [{}],
};

const getWrapper = ({ collection = ['a', 'b', 'c'], onNavigateLeft = noop, onNavigateRight = noop, ...rest }) =>
const deeplinkedMetadataHistoryMock = {
location: { pathname: '/metadata/filteredTemplates/123,124', hash: '' },
listen: jest.fn(),
push: jest.fn(),
entries: [{}],
};

const getWrapper = ({
collection = ['a', 'b', 'c'],
historyMock = historyMockDefault,
onNavigateLeft = noop,
onNavigateRight = noop,
...rest
}) =>
mount(
<Router history={historyMock}>
<PreviewNavigation
Expand Down Expand Up @@ -51,8 +64,8 @@ describe('elements/content-preview/PreviewNavigation', () => {
expect(wrapper.find('PlainButton')).toHaveLength(1);
wrapper.find('PlainButton').simulate('click');

expect(historyMock.push).toBeCalledTimes(1);
expect(historyMock.push).toBeCalledWith('/activity');
expect(historyMockDefault.push).toBeCalledTimes(1);
expect(historyMockDefault.push).toBeCalledWith('/activity');
expect(onNavigateLeftMock).toHaveBeenCalled();
});

Expand All @@ -63,8 +76,8 @@ describe('elements/content-preview/PreviewNavigation', () => {
expect(wrapper.find('PlainButton')).toHaveLength(1);
wrapper.find('PlainButton').simulate('click');

expect(historyMock.push).toBeCalledTimes(1);
expect(historyMock.push).toBeCalledWith('/activity');
expect(historyMockDefault.push).toBeCalledTimes(1);
expect(historyMockDefault.push).toBeCalledWith('/activity');
expect(onNavigateRightMock).toHaveBeenCalled();
});
test('should render navigation correctly from comments deeplinked URL ', () => {
Expand All @@ -74,9 +87,41 @@ describe('elements/content-preview/PreviewNavigation', () => {
expect(wrapper.find('PlainButton')).toHaveLength(1);
wrapper.find('PlainButton').simulate('click');

expect(historyMock.push).toBeCalledTimes(1);
expect(historyMock.push).toBeCalledWith('/activity');
expect(historyMockDefault.push).toBeCalledTimes(1);
expect(historyMockDefault.push).toBeCalledWith('/activity');
expect(onNavigateRightMock).toHaveBeenCalled();
});

test('should render right navigation correctly from metadata deeplinked URL ', () => {
const onNavigateRightMock = jest.fn();
const wrapper = getWrapper({
currentIndex: 0,
historyMock: deeplinkedMetadataHistoryMock,
onNavigateRight: onNavigateRightMock,
});

expect(wrapper.find('PlainButton')).toHaveLength(1);
wrapper.find('PlainButton').simulate('click');

expect(deeplinkedMetadataHistoryMock.push).toBeCalledTimes(1);
expect(deeplinkedMetadataHistoryMock.push).toBeCalledWith('/metadata/filteredTemplates/123,124');
expect(onNavigateRightMock).toHaveBeenCalled();
});

test('should render left navigation correctly from metadata deeplinked URL ', () => {
const onNavigateLeftMock = jest.fn();
const wrapper = getWrapper({
currentIndex: 2,
historyMock: deeplinkedMetadataHistoryMock,
onNavigateLeft: onNavigateLeftMock,
});

expect(wrapper.find('PlainButton')).toHaveLength(1);
wrapper.find('PlainButton').simulate('click');

expect(deeplinkedMetadataHistoryMock.push).toBeCalledTimes(1);
expect(deeplinkedMetadataHistoryMock.push).toBeCalledWith('/metadata/filteredTemplates/123,124');
expect(onNavigateLeftMock).toHaveBeenCalled();
});
});
});
30 changes: 21 additions & 9 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import * as React from 'react';
import flow from 'lodash/flow';
import { FormattedMessage, useIntl } from 'react-intl';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { InlineError, LoadingIndicator } from '@box/blueprint-web';
import {
AddMetadataTemplateDropdown,
Expand Down Expand Up @@ -38,6 +39,7 @@ import MetadataInstanceEditor from './MetadataInstanceEditor';
import { convertTemplateToTemplateInstance } from './utils/convertTemplateToTemplateInstance';
import { isExtensionSupportedForMetadataSuggestions } from './utils/isExtensionSupportedForMetadataSuggestions';
import { metadataTaxonomyFetcher, metadataTaxonomyNodeAncestorsFetcher } from './fetchers/metadataTaxonomyFetcher';
import { useMetadataSidebarFilteredTemplates } from './hooks/useMetadataSidebarFilteredTemplates';

const MARK_NAME_JS_READY = `${ORIGIN_METADATA_SIDEBAR_REDESIGN}_${EVENT_JS_READY}`;

Expand All @@ -50,18 +52,31 @@ export interface ExternalProps {
interface PropsWithoutContext extends ExternalProps {
elementId: string;
fileId: string;
filteredTemplateIds?: string[];
hasSidebarInitialized?: boolean;
}

export interface ErrorContextProps {
onError: (error: Error, code: string, contextInfo?: Record<string, unknown>) => void;
}

export interface MetadataSidebarRedesignProps extends PropsWithoutContext, ErrorContextProps, WithLoggerProps {
export interface MetadataSidebarRedesignProps
extends PropsWithoutContext,
ErrorContextProps,
WithLoggerProps,
RouteComponentProps {
api: API;
}

function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEnabled }: MetadataSidebarRedesignProps) {
function MetadataSidebarRedesign({
api,
elementId,
fileId,
filteredTemplateIds = [],
history,
onError,
isFeatureEnabled,
}: MetadataSidebarRedesignProps) {
const {
extractSuggestions,
file,
Expand Down Expand Up @@ -173,21 +188,17 @@ function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEna
/>
);

const [filteredTemplates, setFilteredTemplates] = React.useState([]);
const { handleSetFilteredTemplates, filteredTemplates, templateInstancesList } =
useMetadataSidebarFilteredTemplates(history, filteredTemplateIds, templateInstances);
const filterDropdown =
isSuccess && isViewMode && appliedTemplateInstances.length > 1 ? (
<FilterInstancesDropdown
appliedTemplates={appliedTemplateInstances as MetadataTemplate[]}
selectedTemplates={filteredTemplates}
setSelectedTemplates={setFilteredTemplates}
setSelectedTemplates={handleSetFilteredTemplates}
/>
) : null;

const filteredTemplateInstances = templateInstances.filter(instance =>
filteredTemplates.some(template => template === instance.id),
);
const templateInstancesList = filteredTemplates.length === 0 ? templateInstances : filteredTemplateInstances;

const errorMessageDisplay = status === STATUS.ERROR && errorMessage && (
<InlineError className="bcs-MetadataSidebarRedesign-inline-error">
<FormattedMessage {...errorMessage} />
Expand Down Expand Up @@ -259,6 +270,7 @@ function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEna

export { MetadataSidebarRedesign as MetadataSidebarRedesignComponent };
export default flow([
withRouter,
withLogger(ORIGIN_METADATA_SIDEBAR_REDESIGN),
withErrorBoundary(ORIGIN_METADATA_SIDEBAR_REDESIGN),
withAPIContext,
Expand Down
12 changes: 10 additions & 2 deletions src/elements/content-sidebar/SidebarPanels.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,13 +356,21 @@ class SidebarPanels extends React.Component<Props, State> {
{hasMetadata && (
<Route
exact
path={`/${SIDEBAR_VIEW_METADATA}`}
render={() => {
path={[
`/${SIDEBAR_VIEW_METADATA}`,
`/${SIDEBAR_VIEW_METADATA}/filteredTemplates/:filteredTemplateIds?`,
]}
render={({ match }) => {
this.handlePanelRender(SIDEBAR_VIEW_METADATA);
return isMetadataSidebarRedesignEnabled ? (
<LoadableMetadataSidebarRedesigned
elementId={elementId}
fileId={fileId}
filteredTemplateIds={
match.params.filteredTemplateIds
? match.params.filteredTemplateIds.split(',')
: []
}
hasSidebarInitialized={isInitialized}
isBoxAiSuggestionsEnabled={isMetadataAiSuggestionsEnabled}
ref={this.metadataSidebar}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { userEvent } from '@testing-library/user-event';
import { RouteComponentProps } from 'react-router-dom';
import { type MetadataTemplate, type MetadataTemplateInstance } from '@box/metadata-editor';
import { FIELD_PERMISSIONS_CAN_UPLOAD } from '../../../constants';
import { screen, render } from '../../../test-utils/testing-library';
Expand Down Expand Up @@ -87,12 +88,16 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => {
};

const renderComponent = (props = {}, features = {}) => {
const emptyFilteredTemplateIds = [];
const routeComponentProps = {} as RouteComponentProps;
const defaultProps = {
api: {},
fileId: 'test-file-id-1',
elementId: 'element-1',
filteredTemplateIds: emptyFilteredTemplateIds,
isFeatureEnabled: true,
onError: jest.fn(),
...routeComponentProps,
} satisfies MetadataSidebarRedesignProps;

render(<MetadataSidebarRedesign {...defaultProps} {...props} />, { wrapperProps: { features } });
Expand Down Expand Up @@ -164,6 +169,26 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => {
expect(customMetadataOption).toHaveAttribute('aria-disabled', 'true');
});

test('should have accessible "All templates" combobox trigger button', () => {
mockUseSidebarMetadataFetcher.mockReturnValue({
extractSuggestions: jest.fn(),
handleCreateMetadataInstance: jest.fn(),
handleDeleteMetadataInstance: jest.fn(),
handleUpdateMetadataInstance: jest.fn(),
templateInstances: [mockTemplateInstance, mockCustomTemplateInstance],
templates: mockTemplates,
errorMessage: null,
status: STATUS.SUCCESS,
file: mockFile,
});

renderComponent();

expect(
screen.getAllByRole('combobox').find(combobox => combobox.textContent === 'All Templates'),
).toBeInTheDocument();
});

test('should render metadata sidebar with error', async () => {
mockUseSidebarMetadataFetcher.mockReturnValue({
extractSuggestions: jest.fn(),
Expand Down Expand Up @@ -272,4 +297,51 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => {

expect(screen.getByRole('heading', { level: 4, name: 'Visible Template' })).toBeInTheDocument();
});

test('should render metadata filterd instance list when fileterd templates are present and matching', () => {
mockUseSidebarMetadataFetcher.mockReturnValue({
extractSuggestions: jest.fn(),
handleCreateMetadataInstance: jest.fn(),
handleDeleteMetadataInstance: jest.fn(),
handleUpdateMetadataInstance: jest.fn(),
templateInstances: [mockCustomTemplateInstance, mockVisibleTemplateInstance],
templates: mockTemplates,
errorMessage: null,
status: STATUS.SUCCESS,
file: mockFile,
});

const filteredTemplateIds = [mockVisibleTemplateInstance.id];

renderComponent({ filteredTemplateIds });

expect(screen.getByRole('heading', { level: 3, name: 'Metadata' })).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 4, name: 'Visible Template' })).toBeInTheDocument();
expect(screen.queryByRole('heading', { level: 4, name: 'Custom Metadata' })).not.toBeInTheDocument();
});

test('should render metadata unfiltered instance list when fileterd templates are present and do not match existing templates', () => {
mockUseSidebarMetadataFetcher.mockReturnValue({
extractSuggestions: jest.fn(),
handleCreateMetadataInstance: jest.fn(),
handleDeleteMetadataInstance: jest.fn(),
handleUpdateMetadataInstance: jest.fn(),
templateInstances: [mockCustomTemplateInstance, mockVisibleTemplateInstance],
templates: mockTemplates,
errorMessage: null,
status: STATUS.SUCCESS,
file: mockFile,
});

const filteredTemplateIds = ['non-existing-template-id'];

renderComponent({ filteredTemplateIds });

expect(screen.getByRole('heading', { level: 3, name: 'Metadata' })).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 4, name: 'Custom Metadata' })).toBeInTheDocument();
expect(screen.getByText(mockCustomTemplateInstance.fields[0].key)).toBeInTheDocument();
expect(screen.getByText(mockCustomTemplateInstance.fields[1].key)).toBeInTheDocument();

expect(screen.getByRole('heading', { level: 4, name: 'Visible Template' })).toBeInTheDocument();
});
});
Loading

0 comments on commit 828b527

Please sign in to comment.