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

feat(metadata-sidebar): Add handler for Autofill button #3700

Merged
merged 7 commits into from
Oct 10, 2024
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
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}`,
Copy link
Contributor

@tjuanitas tjuanitas Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see we do this in the ask method as well but how important is this id field? if items has multiple files would this cause conflicts or unexpected behavior since we always look at the first index?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id is used internally by post to get auth token, it can by an array, but for this endpoint we only use one file. That is whybI take first element.

});
} 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
Loading