From 21d879a9ea9a61117069b1847be643e9e7d65d91 Mon Sep 17 00:00:00 2001 From: Dawid Jankowiak Date: Mon, 7 Oct 2024 18:40:30 +0200 Subject: [PATCH] feat(metadata-sidebar): Add handler for Autofill button --- package.json | 4 +- src/api/Intelligence.js | 1 + src/api/__tests__/Intelligence.test.js | 4 ++ .../MetadataInstanceEditor.tsx | 40 +++++++------------ .../MetadataSidebarRedesign.tsx | 20 ++++------ .../__tests__/MetadataInstanceEditor.test.tsx | 3 +- .../MetadataSidebarRedesign.test.tsx | 15 +++++-- .../hooks/useSidebarMetadataFetcher.ts | 40 ++++++++++++++++++- ...MetadataSidebarRedesign-visual.stories.tsx | 2 +- src/test-utils/testing-library.tsx | 7 ++-- yarn.lock | 36 ++++------------- 11 files changed, 92 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index eb57302ba5..7e0f16de05 100644 --- a/package.json +++ b/package.json @@ -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.61.2", "@box/react-virtualized": "9.22.3-rc-box.9", "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@chromatic-com/storybook": "^1.6.1", @@ -306,7 +306,7 @@ "@box/blueprint-web-assets": "^4.21.0", "@box/box-ai-content-answers": "^0.50.5", "@box/cldr-data": ">=34.2.0", - "@box/metadata-editor": "^0.61.1", + "@box/metadata-editor": "^0.61.2", "@box/react-virtualized": "9.22.3-rc-box.9", "@hapi/address": "^2.1.4", "axios": "^0.25.0", diff --git a/src/api/Intelligence.js b/src/api/Intelligence.js index 06d8ef3c17..a609db21e8 100644 --- a/src/api/Intelligence.js +++ b/src/api/Intelligence.js @@ -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; diff --git a/src/api/__tests__/Intelligence.test.js b/src/api/__tests__/Intelligence.test.js index 94a6ad37c9..452598f863 100644 --- a/src/api/__tests__/Intelligence.test.js +++ b/src/api/__tests__/Intelligence.test.js @@ -102,6 +102,7 @@ describe('api/Intelligence', () => { describe('extractStructured()', () => { const request = { + items: [{ id: '123', type: 'file' }], metadata_template: { type: 'metadata_template', scope: 'global', @@ -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, }); }); @@ -143,6 +145,7 @@ describe('api/Intelligence', () => { expect(intelligence.xhr.post).toHaveBeenCalledWith({ url: `${intelligence.getBaseApiUrl()}/ai/extract_structured`, data: request, + id: 'file_123', }); }); @@ -161,6 +164,7 @@ describe('api/Intelligence', () => { expect(intelligence.xhr.post).toHaveBeenCalledWith({ url: `${intelligence.getBaseApiUrl()}/ai/extract_structured`, data: request, + id: 'file_123', }); }); }); diff --git a/src/elements/content-sidebar/MetadataInstanceEditor.tsx b/src/elements/content-sidebar/MetadataInstanceEditor.tsx index d945a4d33d..1b3cd05f68 100644 --- a/src/elements/content-sidebar/MetadataInstanceEditor.tsx +++ b/src/elements/content-sidebar/MetadataInstanceEditor.tsx @@ -1,7 +1,6 @@ import { - AutofillContextProvider, - AutofillContextProviderProps, MetadataInstanceForm, + withApiWrapper, type FormValues, type JSONPatchOperations, type MetadataTemplateInstance, @@ -10,7 +9,6 @@ import React from 'react'; export interface MetadataInstanceEditorProps { areAiSuggestionsAvailable: boolean; - fetchSuggestions: AutofillContextProviderProps['fetchSuggestions']; isBoxAiSuggestionsEnabled: boolean; isDeleteButtonDisabled: boolean; isUnsavedChangesModalOpen: boolean; @@ -22,9 +20,8 @@ export interface MetadataInstanceEditorProps { template: MetadataTemplateInstance; } -const MetadataInstanceEditor: React.FC = ({ +export const MetadataInstanceEditor: React.FC = ({ areAiSuggestionsAvailable, - fetchSuggestions, isBoxAiSuggestionsEnabled, isDeleteButtonDisabled, isUnsavedChangesModalOpen, @@ -35,29 +32,20 @@ const MetadataInstanceEditor: React.FC = ({ setIsUnsavedChangesModalOpen, template, }) => { - const handleCancel = () => { - onCancel(); - }; - return ( - - - + isDeleteButtonDisabled={isDeleteButtonDisabled} + isUnsavedChangesModalOpen={isUnsavedChangesModalOpen} + onCancel={onCancel} + onDelete={onDelete} + onDiscardUnsavedChanges={onDiscardUnsavedChanges} + onSubmit={onSubmit} + selectedTemplateInstance={template} + setIsUnsavedChangesModalOpen={setIsUnsavedChangesModalOpen} + /> ); }; -export default MetadataInstanceEditor; +export default withApiWrapper(MetadataInstanceEditor); diff --git a/src/elements/content-sidebar/MetadataSidebarRedesign.tsx b/src/elements/content-sidebar/MetadataSidebarRedesign.tsx index c53c852569..7a3a4b51b2 100644 --- a/src/elements/content-sidebar/MetadataSidebarRedesign.tsx +++ b/src/elements/content-sidebar/MetadataSidebarRedesign.tsx @@ -15,16 +15,15 @@ import { type MetadataTemplateInstance, type MetadataTemplate, } 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'; @@ -34,7 +33,7 @@ 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'; @@ -67,6 +66,7 @@ export interface MetadataSidebarRedesignProps extends PropsWithoutContext, Error function MetadataSidebarRedesign({ api, elementId, fileId, onError, isFeatureEnabled }: MetadataSidebarRedesignProps) { const { + extractSuggestions, file, handleCreateMetadataInstance, handleDeleteMetadataInstance, @@ -174,13 +174,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( - async (templateKey, fields) => { - // should use getIntelligenceAPI().extractStructured - return fields; - }, - [], - ); return ( { + setEditingTemplate(templateInstance); + }} templateInstances={templateInstances} /> )} diff --git a/src/elements/content-sidebar/__tests__/MetadataInstanceEditor.test.tsx b/src/elements/content-sidebar/__tests__/MetadataInstanceEditor.test.tsx index 86c8d3af1a..1ac7a7f954 100644 --- a/src/elements/content-sidebar/__tests__/MetadataInstanceEditor.test.tsx +++ b/src/elements/content-sidebar/__tests__/MetadataInstanceEditor.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { type MetadataTemplateInstance } from '@box/metadata-editor'; import userEvent from '@testing-library/user-event'; import { screen, render } from '../../../test-utils/testing-library'; -import MetadataInstanceEditor, { MetadataInstanceEditorProps } from '../MetadataInstanceEditor'; +import { MetadataInstanceEditor, MetadataInstanceEditorProps } from '../MetadataInstanceEditor'; const mockOnCancel = jest.fn(); const mockOnDiscardUnsavedChanges = jest.fn(); @@ -50,7 +50,6 @@ describe('MetadataInstanceEditor', () => { const defaultProps: MetadataInstanceEditorProps = { areAiSuggestionsAvailable: true, - fetchSuggestions: jest.fn(), isBoxAiSuggestionsEnabled: true, isDeleteButtonDisabled: false, isUnsavedChangesModalOpen: false, diff --git a/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx b/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx index 45e97b1aae..bebe2dd24d 100644 --- a/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx +++ b/src/elements/content-sidebar/__tests__/MetadataSidebarRedesign.test.tsx @@ -68,8 +68,9 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { beforeEach(() => { mockUseSidebarMetadataFetcher.mockReturnValue({ - handleDeleteMetadataInstance: jest.fn(), + extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), + handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templates: mockTemplates, templateInstances: [], @@ -113,8 +114,9 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata sidebar with error', async () => { mockUseSidebarMetadataFetcher.mockReturnValue({ - handleDeleteMetadataInstance: jest.fn(), + extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), + handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [], templates: [], @@ -135,6 +137,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata sidebar with loading indicator', async () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), @@ -163,7 +166,10 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { }); test('should correctly render empty state when AI feature is disabled', () => { - renderComponent({ isBoxAiSuggestionsEnabled: false }); + renderComponent( + { isBoxAiSuggestionsEnabled: false }, + { wrapperProps: { features: { 'metadata.aiSuggestions.enabled': false } } }, + ); expect(screen.getByRole('heading', { level: 2, name: 'Add Metadata Templates' })).toBeInTheDocument(); expect( screen.getByText('Add Metadata to your file to support business operations, workflows, and more!'), @@ -172,8 +178,9 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata instance list when templates are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ - handleDeleteMetadataInstance: jest.fn(), + extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), + handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockCustomTemplateInstance], templates: mockTemplates, diff --git a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts index 67b0f1961f..6f760c4833 100644 --- a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts +++ b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts @@ -1,7 +1,12 @@ import * as React from 'react'; import getProp from 'lodash/get'; import { type MessageDescriptor } from 'react-intl'; -import { type JSONPatchOperations, type MetadataTemplate, type MetadataTemplateInstance } from '@box/metadata-editor'; +import { + type JSONPatchOperations, + type MetadataTemplate, + type MetadataTemplateInstance, + type MetadataTemplateField, +} from '@box/metadata-editor'; import API from '../../../api'; import { type ElementsXhrError } from '../../../common/types/api'; import { isUserCorrectableError } from '../../../utils/error'; @@ -20,6 +25,7 @@ export enum STATUS { } interface DataFetcher { errorMessage: MessageDescriptor | null; + extractSuggestions: (templateKey: string, fields: MetadataTemplateField[]) => Promise; file: BoxItem | null; handleCreateMetadataInstance: ( templateInstance: MetadataTemplateInstance, @@ -174,6 +180,37 @@ function useSidebarMetadataFetcher( [api, file, onApiError], ); + const extractSuggestions = React.useCallback( + async (templateKey: string, fields: MetadataTemplateField[]) => { + const aiAPI = api.getIntelligenceAPI(); + let answer = {}; + try { + answer = await aiAPI.extractStructured({ + items: [file], + metadata_template: { + scope: 'enterprise', + template_key: templateKey, + type: 'metadata_template', + }, + }); + } catch (e) { + // eslint-disable-next-line no-console + } + + return fields.map(field => { + const value = answer[field.key]; + if (!value) { + return field; + } + return { + ...field, + aiSuggestion: value, + }; + }); + }, + [api, file], + ); + React.useEffect(() => { if (status === STATUS.IDLE) { setStatus(STATUS.LOADING); @@ -185,6 +222,7 @@ function useSidebarMetadataFetcher( }, [api, fetchFileErrorCallback, fetchFileSuccessCallback, fileId, status]); return { + extractSuggestions, handleCreateMetadataInstance, handleDeleteMetadataInstance, handleUpdateMetadataInstance, diff --git a/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx b/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx index d99de05ff8..d97bedba9b 100644 --- a/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx +++ b/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx @@ -206,7 +206,7 @@ export const MetadataInstanceEditorCancelChanges: StoryObj heading.textContent)).toEqual(expect.arrayContaining(['Custom Metadata'])); + expect(headlines.map(heading => heading.textContent)).toEqual(expect.arrayContaining(['My Template'])); // cancel editing - back to list view const cancelButton = await canvas.findByRole('button', { name: 'Cancel' }); diff --git a/src/test-utils/testing-library.tsx b/src/test-utils/testing-library.tsx index 172c0fb36e..71afc73776 100644 --- a/src/test-utils/testing-library.tsx +++ b/src/test-utils/testing-library.tsx @@ -4,7 +4,8 @@ import { render, type RenderOptions } from '@testing-library/react'; // Data Providers import { TooltipProvider } from '@box/blueprint-web'; import { IntlProvider } from 'react-intl'; -import { AutofillContextProvider } from '@box/metadata-editor'; +import { AutofillContextProvider, MetadataTemplateField } from '@box/metadata-editor'; + import { FeatureProvider } from '../elements/common/feature-checking'; jest.unmock('react-intl'); @@ -12,12 +13,12 @@ jest.unmock('react-intl'); const Wrapper = ({ children, features = {}, - isAiSuggestionsFeatureEnabled = false, fetchSuggestions = () => Promise.resolve([]), + isAiSuggestionsFeatureEnabled = false, }) => ( diff --git a/yarn.lock b/yarn.lock index ac7f49aae7..77551666f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1569,7 +1569,7 @@ resolved "https://registry.yarnpkg.com/@box/languages/-/languages-1.1.2.tgz#cd4266b3da62da18560d881e10b429653186be29" integrity sha512-d64TGosx+KRmrLZj4CIyLp42LUiEbgBJ8n8cviMQwTJmfU0g+UwZqLjmQZR1j+Q9D64yV4xHzY9K1t5nInWWeQ== -"@box/metadata-editor@^0.61.1": +"@box/metadata-editor@^0.61.2": version "0.61.2" resolved "https://registry.yarnpkg.com/@box/metadata-editor/-/metadata-editor-0.61.2.tgz#b8e66f3e822531ae57463cfdd8be89dd9cabdb21" integrity sha512-Ot+cGU+benO0ChRrKWahvJmHMgxKTp4nuNr+VypnQtTt63hGCn1F8TbZ2aQNRtPikhoGCAiOwYspG8fRKJ/Fmw== @@ -22435,7 +22435,8 @@ string-replace-loader@^3.1.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22453,15 +22454,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -22600,7 +22592,8 @@ stringify-package@^1.0.0, stringify-package@^1.0.1: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22628,13 +22621,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -24839,7 +24825,8 @@ worker-farm@^1.6.0, worker-farm@^1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -24882,15 +24869,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"