Skip to content

Commit

Permalink
feat(metadata-sidebar): Add handler for Autofill button (#3700)
Browse files Browse the repository at this point in the history
* feat(metadata-sidebar): Add handler for Autofill button

* feat(metadata-sidebar): Handle Error by rethrowing them to host app

* feat(metadata-sidebar): Add error handling

And bump metadata-editor package
Wrap MetadataInstanceList and MetadataInstanceForm in Autofill provider

Co-authored-by: Wiola (wpiesiak)

* feat(metadata-sidebar): Remove unnecessary wait function

* feat(metadata-sidebar): Restore old export in MetadataInstanceEditor

* feat(metadata-sidebar): Move AutofillContextProvider out of test-utils

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
jankowiakdawid and mergify[bot] authored Oct 10, 2024
1 parent 21c2de1 commit 95735e0
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 115 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
"@box/cldr-data": "^34.2.0",
"@box/frontend": "^10.0.0",
"@box/languages": "^1.0.0",
"@box/metadata-editor": "^0.61.1",
"@box/metadata-editor": "^0.65.0",
"@box/react-virtualized": "9.22.3-rc-box.9",
"@cfaester/enzyme-adapter-react-18": "^0.8.0",
"@chromatic-com/storybook": "^1.6.1",
Expand Down Expand Up @@ -306,7 +306,7 @@
"@box/blueprint-web-assets": "^4.21.0",
"@box/box-ai-content-answers": "^0.57.1",
"@box/cldr-data": ">=34.2.0",
"@box/metadata-editor": "^0.61.1",
"@box/metadata-editor": "^0.65.0",
"@box/react-virtualized": "9.22.3-rc-box.9",
"@hapi/address": "^2.1.4",
"axios": "^0.25.0",
Expand Down
1 change: 1 addition & 0 deletions src/api/Intelligence.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Intelligence extends Base {
suggestionsResponse = await this.xhr.post({
url,
data: request,
id: `file_${request.items[0].id}`,
});
} catch (e) {
const { status } = e;
Expand Down
4 changes: 4 additions & 0 deletions src/api/__tests__/Intelligence.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe('api/Intelligence', () => {

describe('extractStructured()', () => {
const request = {
items: [{ id: '123', type: 'file' }],
metadata_template: {
type: 'metadata_template',
scope: 'global',
Expand All @@ -124,6 +125,7 @@ describe('api/Intelligence', () => {
expect(suggestions).toEqual(suggestionsFromServer);
expect(intelligence.xhr.post).toHaveBeenCalledWith({
url: `${intelligence.getBaseApiUrl()}/ai/extract_structured`,
id: 'file_123',
data: request,
});
});
Expand All @@ -143,6 +145,7 @@ describe('api/Intelligence', () => {
expect(intelligence.xhr.post).toHaveBeenCalledWith({
url: `${intelligence.getBaseApiUrl()}/ai/extract_structured`,
data: request,
id: 'file_123',
});
});

Expand All @@ -161,6 +164,7 @@ describe('api/Intelligence', () => {
expect(intelligence.xhr.post).toHaveBeenCalledWith({
url: `${intelligence.getBaseApiUrl()}/ai/extract_structured`,
data: request,
id: 'file_123',
});
});
});
Expand Down
40 changes: 16 additions & 24 deletions src/elements/content-sidebar/MetadataInstanceEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import {
AutofillContextProvider,
AutofillContextProviderProps,
MetadataInstanceForm,
type FormValues,
type JSONPatchOperations,
type MetadataTemplateInstance,
type FetcherResponse,
type BaseOptionType,
} from '@box/metadata-editor';
import React from 'react';

const noopTaxonomyFetcher = () => Promise.resolve({ options: [] } satisfies FetcherResponse<BaseOptionType>);

export interface MetadataInstanceEditorProps {
areAiSuggestionsAvailable: boolean;
fetchSuggestions: AutofillContextProviderProps['fetchSuggestions'];
isBoxAiSuggestionsEnabled: boolean;
isDeleteButtonDisabled: boolean;
isUnsavedChangesModalOpen: boolean;
Expand All @@ -24,7 +25,6 @@ export interface MetadataInstanceEditorProps {

const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
areAiSuggestionsAvailable,
fetchSuggestions,
isBoxAiSuggestionsEnabled,
isDeleteButtonDisabled,
isUnsavedChangesModalOpen,
Expand All @@ -35,28 +35,20 @@ const MetadataInstanceEditor: React.FC<MetadataInstanceEditorProps> = ({
setIsUnsavedChangesModalOpen,
template,
}) => {
const handleCancel = () => {
onCancel();
};

return (
<AutofillContextProvider
fetchSuggestions={fetchSuggestions}
<MetadataInstanceForm
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
isAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled}
>
<MetadataInstanceForm
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
isAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled}
isDeleteButtonDisabled={isDeleteButtonDisabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={handleCancel}
onDelete={onDelete}
onDiscardUnsavedChanges={onDiscardUnsavedChanges}
onSubmit={onSubmit}
selectedTemplateInstance={template}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
/>
</AutofillContextProvider>
isDeleteButtonDisabled={isDeleteButtonDisabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={onCancel}
onDelete={onDelete}
onDiscardUnsavedChanges={onDiscardUnsavedChanges}
onSubmit={onSubmit}
selectedTemplateInstance={template}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
taxonomyOptionsFetcher={noopTaxonomyFetcher}
/>
);
};

Expand Down
81 changes: 36 additions & 45 deletions src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,31 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { InlineError, LoadingIndicator } from '@box/blueprint-web';
import {
AddMetadataTemplateDropdown,
AutofillContextProvider,
MetadataEmptyState,
MetadataInstanceList,
type FormValues,
type JSONPatchOperations,
type MetadataTemplateInstance,
type MetadataTemplate,
type MetadataTemplateInstance,
} from '@box/metadata-editor';
import noop from 'lodash/noop';

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 { useFeatureEnabled } from '../common/feature-checking';
import { ORIGIN_METADATA_SIDEBAR_REDESIGN, SIDEBAR_VIEW_METADATA } from '../../constants';
import { EVENT_JS_READY } from '../common/logger/constants';
import { useFeatureEnabled } from '../common/feature-checking';
import { mark } from '../../utils/performance';
import useSidebarMetadataFetcher, { STATUS } from './hooks/useSidebarMetadataFetcher';

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

import messages from '../common/messages';
import './MetadataSidebarRedesign.scss';
import MetadataInstanceEditor, { MetadataInstanceEditorProps } from './MetadataInstanceEditor';
import MetadataInstanceEditor from './MetadataInstanceEditor';
import { convertTemplateToTemplateInstance } from './utils/convertTemplateToTemplateInstance';
import { isExtensionSupportedForMetadataSuggestions } from './utils/isExtensionSupportedForMetadataSuggestions';

Expand All @@ -52,13 +50,8 @@ interface PropsWithoutContext extends ExternalProps {
hasSidebarInitialized?: boolean;
}

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

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

export interface MetadataSidebarRedesignProps extends PropsWithoutContext, ErrorContextProps, WithLoggerProps {
Expand All @@ -67,6 +60,7 @@ export interface MetadataSidebarRedesignProps extends PropsWithoutContext, Error

function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEnabled }: MetadataSidebarRedesignProps) {
const {
extractSuggestions,
file,
handleCreateMetadataInstance,
handleDeleteMetadataInstance,
Expand Down Expand Up @@ -174,13 +168,6 @@ function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEna
const showEditor = !showEmptyState && editingTemplate;
const showList = !showEditor && templateInstances.length > 0 && !editingTemplate;
const areAiSuggestionsAvailable = isExtensionSupportedForMetadataSuggestions(file?.extension ?? '');
const fetchSuggestions = React.useCallback<MetadataInstanceEditorProps['fetchSuggestions']>(
async (templateKey, fields) => {
// should use getIntelligenceAPI().extractStructured
return fields;
},
[],
);

return (
<SidebarContent
Expand All @@ -196,32 +183,36 @@ function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEna
{showEmptyState && (
<MetadataEmptyState level={'file'} isBoxAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled} />
)}
{editingTemplate && (
<MetadataInstanceEditor
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
fetchSuggestions={fetchSuggestions}
isBoxAiSuggestionsEnabled={isBoxAiSuggestionsEnabled}
isDeleteButtonDisabled={isDeleteButtonDisabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={handleCancel}
onDelete={handleDeleteInstance}
onDiscardUnsavedChanges={handleDiscardUnsavedChanges}
onSubmit={handleSubmit}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
template={editingTemplate}
/>
)}
{showList && (
<MetadataInstanceList
isAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled}
onEdit={templateInstance => {
setEditingTemplate(templateInstance);
setIsDeleteButtonDisabled(false);
}}
onEditWithAutofill={noop}
templateInstances={templateInstances}
/>
)}
<AutofillContextProvider
fetchSuggestions={extractSuggestions}
isAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled}
>
{editingTemplate && (
<MetadataInstanceEditor
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
isBoxAiSuggestionsEnabled={isBoxAiSuggestionsEnabled}
isDeleteButtonDisabled={isDeleteButtonDisabled}
isUnsavedChangesModalOpen={isUnsavedChangesModalOpen}
onCancel={handleCancel}
onDelete={handleDeleteInstance}
onDiscardUnsavedChanges={handleDiscardUnsavedChanges}
onSubmit={handleSubmit}
setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen}
template={editingTemplate}
/>
)}
{showList && (
<MetadataInstanceList
areAiSuggestionsAvailable={areAiSuggestionsAvailable}
isAiSuggestionsFeatureEnabled={isBoxAiSuggestionsEnabled}
onEdit={templateInstance => {
setEditingTemplate(templateInstance);
setIsDeleteButtonDisabled(false);
}}
templateInstances={templateInstances}
/>
)}
</AutofillContextProvider>
</div>
</SidebarContent>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import React from 'react';
import { type MetadataTemplateInstance } from '@box/metadata-editor';
import { AutofillContextProvider, type MetadataTemplateInstance } from '@box/metadata-editor';
import userEvent from '@testing-library/user-event';
import { TooltipProvider } from '@box/blueprint-web';
import { IntlProvider } from 'react-intl';
import { screen, render } from '../../../test-utils/testing-library';
import MetadataInstanceEditor, { MetadataInstanceEditorProps } from '../MetadataInstanceEditor';
import { FeatureProvider } from '../../common/feature-checking';

const mockOnCancel = jest.fn();
const mockOnDiscardUnsavedChanges = jest.fn();
const mockSetIsUnsavedChangesModalOpen = jest.fn();

jest.unmock('react-intl');

const wrapper = ({ children }) => (
<AutofillContextProvider fetchSuggestions={() => Promise.resolve([])} isAiSuggestionsFeatureEnabled>
<FeatureProvider features={{}}>
<TooltipProvider>
<IntlProvider locale="en">{children}</IntlProvider>
</TooltipProvider>
</FeatureProvider>
</AutofillContextProvider>
);

const renderWithAutofill = element => render(element, { wrapper });

describe('MetadataInstanceEditor', () => {
const mockCustomMetadataTemplate: MetadataTemplateInstance = {
id: 'template-id',
Expand Down Expand Up @@ -50,7 +67,6 @@ describe('MetadataInstanceEditor', () => {

const defaultProps: MetadataInstanceEditorProps = {
areAiSuggestionsAvailable: true,
fetchSuggestions: jest.fn(),
isBoxAiSuggestionsEnabled: true,
isDeleteButtonDisabled: false,
isUnsavedChangesModalOpen: false,
Expand All @@ -63,48 +79,48 @@ describe('MetadataInstanceEditor', () => {
};

test('should render MetadataInstanceForm with correct props', () => {
render(<MetadataInstanceEditor {...defaultProps} />);
renderWithAutofill(<MetadataInstanceEditor {...defaultProps} />);

const templateHeader = screen.getByText(mockMetadataTemplateInstance.displayName);
expect(templateHeader).toBeInTheDocument();
});

test('should render MetadataInstanceForm with Custom Template', () => {
const props = { ...defaultProps, template: mockCustomMetadataTemplate };
render(<MetadataInstanceEditor {...props} />);
renderWithAutofill(<MetadataInstanceEditor {...props} />);

const templateHeader = screen.getByText('Custom Metadata');
expect(templateHeader).toBeInTheDocument();
});

test('should render UnsavedChangesModal if isUnsavedChangesModalOpen is true', async () => {
const props = { ...defaultProps, isUnsavedChangesModalOpen: true };
const { findByText } = render(<MetadataInstanceEditor {...props} />);
const { findByText } = renderWithAutofill(<MetadataInstanceEditor {...props} />);

const unsavedChangesModal = await findByText('Unsaved Changes');
expect(unsavedChangesModal).toBeInTheDocument();
});

test('should render MetadataInstanceForm with Delete button disabled', () => {
const props = { ...defaultProps, isDeleteButtonDisabled: true };
render(<MetadataInstanceEditor {...props} />);
renderWithAutofill(<MetadataInstanceEditor {...props} />);

const deleteButton = screen.getByRole('button', { name: 'Delete' });
expect(deleteButton).toBeDisabled();
});

test('should render MetadataInstanceForm with Delete button enabled', () => {
render(<MetadataInstanceEditor {...defaultProps} />);
renderWithAutofill(<MetadataInstanceEditor {...defaultProps} />);

const deleteButton = screen.getByRole('button', { name: 'Delete' });
expect(deleteButton).toBeEnabled();
});

test('Should call onCancel when canceling editing', async () => {
const props: MetadataInstanceEditorProps = { ...defaultProps, template: mockCustomMetadataTemplate };
const { findByRole } = render(<MetadataInstanceEditor {...props} />);
const cancelButton = await findByRole('button', { name: 'Cancel' });
renderWithAutofill(<MetadataInstanceEditor {...props} />);

const cancelButton = await screen.findByRole('button', { name: 'Cancel' });
await userEvent.click(cancelButton);

expect(mockOnCancel).toHaveBeenCalled();
Expand All @@ -115,9 +131,10 @@ describe('MetadataInstanceEditor', () => {
...defaultProps,
template: mockCustomMetadataTemplateWithField,
};
const { rerender, findByRole, findByText } = render(<MetadataInstanceEditor {...props} />);
const input = await findByRole('textbox');
const cancelButton = await findByRole('button', { name: 'Cancel' });
const { rerender } = renderWithAutofill(<MetadataInstanceEditor {...props} />);

const input = await screen.findByRole('textbox');
const cancelButton = await screen.findByRole('button', { name: 'Cancel' });

await userEvent.type(input, 'Lorem ipsum dolor.');
await userEvent.click(cancelButton);
Expand All @@ -126,10 +143,11 @@ describe('MetadataInstanceEditor', () => {
expect(mockSetIsUnsavedChangesModalOpen).toHaveBeenCalledWith(true);

rerender(<MetadataInstanceEditor {...props} isUnsavedChangesModalOpen={true} />);
const unsavedChangesModal = await findByText('Unsaved Changes');

const unsavedChangesModal = await screen.findByText('Unsaved Changes');

expect(unsavedChangesModal).toBeInTheDocument();
const unsavedChangesModalDiscardButton = await findByRole('button', { name: 'Discard Changes' });
const unsavedChangesModalDiscardButton = await screen.findByRole('button', { name: 'Discard Changes' });

await userEvent.click(unsavedChangesModalDiscardButton);

Expand Down
Loading

0 comments on commit 95735e0

Please sign in to comment.