Skip to content

Commit

Permalink
feat(metadata-sidebar): Add metadata add template dropdown menu (#3606)
Browse files Browse the repository at this point in the history
* feat(metadata-sidebar): Add AddMetadataTemplateDropdown

and improve basic styling

* feat(metadata-sidebar): Fix failing tests

Extend Jest configuration to not transforming metadata-editor code

* feat(metadata-sidebar): add AddMetadataTemplateDropdown

To MetadataSidebarRedesign

* feat(content-sidebar): Bring back changes to mockServiceWorker.js

No idea why they got there in the first place

* feat(metadata-sidebar): update storybook

* feat(metadata-sidebar): PR comments

* feat(metadata-sidebar): simplify storybook

* feat(metadata-sidebar): enum status

* feat(metadata-sidebar): global variables and enum upper case change

* feat(metadata-sidebar): useSidebarMetadataFetcher tests

* feat(metadata-sidebar): useSidebarMetadataFetcher tests

* feat(metadata-sidebar): PR comments

* feat(metadata-sidebar): PR comments

* feat(metadata-sidebar): loading status test

* feat(metadata-sidebar): use SidebarContent + tests

* feat(metadata-sidebar): template dropdown menu nit fixes

---------

Co-authored-by: Karolina Rusek-Bieniek <krusek@box.com>
Co-authored-by: Wiola <wpiesiak@box.com>
  • Loading branch information
3 people authored Aug 23, 2024
1 parent 897a5c7 commit 065c51b
Show file tree
Hide file tree
Showing 10 changed files with 526 additions and 37 deletions.
4 changes: 3 additions & 1 deletion scripts/jest/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ module.exports = {
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.test.+(js|jsx|ts|tsx)'],
testPathIgnorePatterns: ['stories.test.js$', 'stories.test.tsx$', 'stories.test.d.ts'],
transformIgnorePatterns: ['node_modules/(?!(@box/react-virtualized/dist/es|@box/cldr-data|@box/blueprint-web|@box/blueprint-web-assets)/)'],
transformIgnorePatterns: [
'node_modules/(?!(@box/react-virtualized/dist/es|@box/cldr-data|@box/blueprint-web|@box/blueprint-web-assets|@box/metadata-editor)/)',
],
};
9 changes: 7 additions & 2 deletions src/elements/content-sidebar/MetadataSidebarRedesign.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
@import '../common/variables';
@import '~@box/blueprint-web-assets/tokens/tokens.scss';

.bcs-MetadataSidebarRedesign {
padding-inline: 10px;
border-left: 1px solid $gray-10;

.bcs-MetadataSidebarRedesign-content {
padding: $space-2;
background-color: $gray-02;
}
}
85 changes: 74 additions & 11 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,93 @@
*/
import * as React from 'react';
import flow from 'lodash/flow';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { InlineError, LoadingIndicator } from '@box/blueprint-web';
import { AddMetadataTemplateDropdown } from '@box/metadata-editor';

import API from '../../api';
import SidebarContent from './SidebarContent';
import { withAPIContext } from '../common/api-context';
import { withErrorBoundary } from '../common/error-boundary';
import { withLogger } from '../common/logger';
import { ORIGIN_METADATA_SIDEBAR_REDESIGN } from '../../constants';
import { ORIGIN_METADATA_SIDEBAR_REDESIGN, SIDEBAR_VIEW_METADATA } from '../../constants';
import { EVENT_JS_READY } from '../common/logger/constants';
import { mark } from '../../utils/performance';
import messages from '../common/messages';
import useSidebarMetadataFetcher, { STATUS } from './hooks/useSidebarMetadataFetcher';

import { type ElementsXhrError } from '../../common/types/api';
import { type ElementOrigin } from '../common/flowTypes';
import { MetadataTemplate } from '../../common/types/metadata';
import { type WithLoggerProps } from '../../common/types/logging';

import messages from '../common/messages';
import './MetadataSidebarRedesign.scss';

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

mark(MARK_NAME_JS_READY);

function MetadataSidebarRedesign() {
export interface ExternalProps {
isFeatureEnabled: boolean;
}

interface PropsWithoutContext extends ExternalProps {
elementId: string;
fileId: string;
hasSidebarInitialized?: boolean;
}

interface ContextInfo {
isErrorDisplayed: boolean;
error: ElementsXhrError | Error;
}

export interface ErrorContextProps {
onError: (error: ElementsXhrError | Error, code: string, contextInfo?: ContextInfo, origin?: ElementOrigin) => void;
}

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

function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEnabled }: MetadataSidebarRedesignProps) {
const { formatMessage } = useIntl();

const [selectedTemplates, setSelectedTemplates] = React.useState<Array<MetadataTemplate>>([]);

const { templates, errorMessage, status } = useSidebarMetadataFetcher(api, fileId, onError, isFeatureEnabled);

const metadataDropdown = status === STATUS.SUCCESS && templates && (
<AddMetadataTemplateDropdown
availableTemplates={templates}
selectedTemplates={selectedTemplates}
onSelect={(selectedTemplate): void => {
setSelectedTemplates([...selectedTemplates, selectedTemplate]);
}}
/>
);

const errorMessageDisplay = status === STATUS.ERROR && errorMessage && (
<InlineError>
<FormattedMessage {...errorMessage} />
</InlineError>
);

return (
<div className="bcs-MetadataSidebarRedesign">
<h3>
<FormattedMessage {...messages.sidebarMetadataTitle} />
</h3>
<hr />
<p>Hello from Metadata Sidebar redesign</p>
</div>
<SidebarContent
actions={metadataDropdown}
className={'bcs-MetadataSidebarRedesign'}
elementId={elementId}
sidebarView={SIDEBAR_VIEW_METADATA}
title={formatMessage(messages.sidebarMetadataTitle)}
>
<div className="bcs-MetadataSidebarRedesign-content">
{errorMessageDisplay}
{status === STATUS.LOADING && (
<LoadingIndicator aria-label={formatMessage(messages.loading)} data-testid="loading" />
)}
</div>
</SidebarContent>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import { userEvent } from '@testing-library/user-event';
import { FIELD_PERMISSIONS_CAN_UPLOAD } from '../../../constants';
import { screen, render } from '../../../test-utils/testing-library';
import {
MetadataSidebarRedesignComponent as MetadataSidebarRedesign,
type MetadataSidebarRedesignProps,
} from '../MetadataSidebarRedesign';
import useSidebarMetadataFetcher, { STATUS } from '../hooks/useSidebarMetadataFetcher';

jest.mock('../hooks/useSidebarMetadataFetcher');
const mockUseSidebarMetadataFetcher = useSidebarMetadataFetcher as jest.MockedFunction<
typeof useSidebarMetadataFetcher
>;

describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => {
const mockTemplates = [
{
id: 'metadata_template_custom_1',
scope: 'global',
templateKey: 'properties',
hidden: false,
},
];

const mockFile = {
id: '123',
permissions: { [FIELD_PERMISSIONS_CAN_UPLOAD]: true },
};

const renderComponent = (props = {}) => {
const defaultProps = {
api: {},
fileId: 'test-file-id-1',
elementId: 'element-1',
isFeatureEnabled: true,
onError: jest.fn(),
} satisfies MetadataSidebarRedesignProps;

render(<MetadataSidebarRedesign {...defaultProps} {...props} />);
};

beforeEach(() => {
mockUseSidebarMetadataFetcher.mockReturnValue({
templates: mockTemplates,
errorMessage: null,
status: STATUS.SUCCESS,
file: mockFile,
});
});

afterEach(() => {
jest.clearAllMocks();
});

test('should render title', () => {
renderComponent();

expect(screen.getByRole('heading', { level: 3, name: 'Metadata' })).toBeInTheDocument();
});

test('should have accessible "Add template" button', () => {
renderComponent();

expect(screen.getByRole('button', { name: 'Add template' })).toBeInTheDocument();
});

test('should have selectable "Custom Metadata" template in dropdown', async () => {
renderComponent();

const addTemplateButton = screen.getByRole('button', { name: 'Add template' });
await userEvent.click(addTemplateButton);

const customMetadataOption = screen.getByRole('option', { name: 'Custom Metadata' });
expect(customMetadataOption).toBeInTheDocument();
userEvent.click(customMetadataOption);

// instead of below assertions check if template was added when MetadataInstanceList will be implemented
await userEvent.click(addTemplateButton);

expect(customMetadataOption).toHaveAttribute('aria-disabled', 'true');
});

test('should render metadata sidebar with error', async () => {
mockUseSidebarMetadataFetcher.mockReturnValue({
templates: [],
errorMessage: {
id: 'error',
defaultMessage: 'error message',
},
status: STATUS.ERROR,
file: mockFile,
});

const errorMessage = { id: 'error', defaultMessage: 'error message' };
renderComponent();

expect(screen.getByRole('heading', { level: 3, name: 'Metadata' })).toBeInTheDocument();
expect(screen.getByText(errorMessage.defaultMessage)).toBeInTheDocument();
});

test('should render metadata sidebar with loading indicator', async () => {
mockUseSidebarMetadataFetcher.mockReturnValue({
templates: [],
errorMessage: null,
status: STATUS.LOADING,
file: mockFile,
});

renderComponent();

expect(screen.getByRole('heading', { level: 3, name: 'Metadata' })).toBeInTheDocument();
expect(screen.getByTestId('loading')).toBeInTheDocument();
expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument();
});
});

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { renderHook, waitFor } from '../../../test-utils/testing-library';
import messages from '../../common/messages';
import { FIELD_PERMISSIONS_CAN_UPLOAD } from '../../../constants';
import useSidebarMetadataFetcher, { STATUS } from '../hooks/useSidebarMetadataFetcher';

const mockError = {
status: 500,
message: 'Internal Server Error',
};

const mockFile = {
id: '123',
permissions: { [FIELD_PERMISSIONS_CAN_UPLOAD]: true },
};

const mockTemplates = [
{
id: 'metadata_template_custom_1',
scope: 'global',
templateKey: 'properties',
hidden: false,
},
];

const mockAPI = {
getFile: jest.fn((id, successCallback, errorCallback) => {
try {
successCallback(mockFile);
} catch (error) {
errorCallback(error);
}
}),
getMetadata: jest.fn((_file, successCallback, errorCallback) => {
try {
successCallback({
editors: [],
templates: mockTemplates,
});
} catch (error) {
errorCallback(error);
}
}),
};
const api = {
getFileAPI: jest.fn().mockReturnValue(mockAPI),
getMetadataAPI: jest.fn().mockReturnValue(mockAPI),
};

describe('useSidebarMetadataFetcher', () => {
const onErrorMock = jest.fn();
const isFeatureEnabledMock = true;

const setupHook = (fileId = '123') =>
renderHook(() => useSidebarMetadataFetcher(api, fileId, onErrorMock, isFeatureEnabledMock));

test('should fetch the file and metadata successfully', async () => {
const { result } = setupHook();

await waitFor(() => expect(result.current.status).toBe(STATUS.SUCCESS));

expect(result.current.file).toEqual(mockFile);
expect(result.current.templates).toEqual(mockTemplates);
expect(result.current.errorMessage).toBeNull();
});

test('should handle file fetching error', async () => {
mockAPI.getFile.mockImplementation((id, successCallback, errorCallback) =>
errorCallback(mockError, 'file_fetch_error'),
);

const { result } = setupHook();

await waitFor(() => expect(result.current.status).toBe(STATUS.ERROR));

expect(result.current.file).toBeUndefined();
expect(result.current.errorMessage).toBe(messages.sidebarMetadataEditingErrorContent);
expect(onErrorMock).toHaveBeenCalledWith(
mockError,
'file_fetch_error',
expect.objectContaining({
error: mockError,
isErrorDisplayed: true,
}),
);
});

test('should handle metadata fetching error', async () => {
mockAPI.getFile.mockImplementation((id, successCallback) => {
successCallback(mockFile);
});
mockAPI.getMetadata.mockImplementation((file, successCallback, errorCallback) => {
errorCallback(mockError, 'metadata_fetch_error');
});
const { result } = setupHook();

await waitFor(() => expect(result.current.status).toBe(STATUS.ERROR));

expect(result.current.templates).toBeNull();
expect(result.current.errorMessage).toBe(messages.sidebarMetadataFetchingErrorContent);
expect(onErrorMock).toHaveBeenCalledWith(
mockError,
'metadata_fetch_error',
expect.objectContaining({
error: mockError,
isErrorDisplayed: true,
}),
);
});
});
Loading

0 comments on commit 065c51b

Please sign in to comment.