diff --git a/package.json b/package.json index 3c5119f8c5..418acfc05a 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "@box/frontend": "^10.0.0", "@box/item-icon": "^0.9.58", "@box/languages": "^1.0.0", - "@box/metadata-editor": "^0.88.1", + "@box/metadata-editor": "0.91.0", "@box/react-virtualized": "9.22.3-rc-box.9", "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@chromatic-com/storybook": "^1.6.1", diff --git a/src/constants.js b/src/constants.js index 89543e69cd..aa7bf709a4 100644 --- a/src/constants.js +++ b/src/constants.js @@ -324,6 +324,8 @@ export const ERROR_CODE_UNEXPECTED_EXCEPTION = 'unexpected_exception_error'; export const ERROR_CODE_SEARCH = 'search_error'; export const ERROR_CODE_METADATA_QUERY = 'metadata_query_error'; export const ERROR_CODE_METADATA_STRUCTURED_TEXT_REP = 'metadata_structured_text_rep_error'; +export const ERROR_CODE_METADATA_AUTOFILL_TIMEOUT = 'metadata_autofill_timeout_error'; +export const ERROR_CODE_METADATA_PRECONDITION_FAILED = 'metadata_precondition_failed_error'; export const ERROR_CODE_UNKNOWN = 'unknown_error'; /* ------------------ Origins ---------------------- */ diff --git a/src/elements/content-sidebar/MetadataInstanceEditor.tsx b/src/elements/content-sidebar/MetadataInstanceEditor.tsx index e3acf96026..b6c47a5b3a 100644 --- a/src/elements/content-sidebar/MetadataInstanceEditor.tsx +++ b/src/elements/content-sidebar/MetadataInstanceEditor.tsx @@ -8,9 +8,15 @@ import { type PaginationQueryInput, } from '@box/metadata-editor'; import React from 'react'; +import { + ERROR_CODE_METADATA_AUTOFILL_TIMEOUT, + ERROR_CODE_UNKNOWN, + ERROR_CODE_METADATA_PRECONDITION_FAILED, +} from '../../constants'; export interface MetadataInstanceEditorProps { areAiSuggestionsAvailable: boolean; + errorCode?: ERROR_CODE_METADATA_AUTOFILL_TIMEOUT | ERROR_CODE_METADATA_PRECONDITION_FAILED | ERROR_CODE_UNKNOWN; isBetaLanguageEnabled: boolean; isBoxAiSuggestionsEnabled: boolean; isDeleteButtonDisabled: boolean; @@ -32,6 +38,7 @@ export interface MetadataInstanceEditorProps { const MetadataInstanceEditor: React.FC = ({ areAiSuggestionsAvailable, + errorCode, isBetaLanguageEnabled, isBoxAiSuggestionsEnabled, isDeleteButtonDisabled, @@ -47,6 +54,7 @@ const MetadataInstanceEditor: React.FC = ({ return ( { + clearExtractError(); + if (editingTemplate) { setPendingTemplateToEdit(convertTemplateToTemplateInstance(file, selectedTemplate)); setIsUnsavedChangesModalOpen(true); @@ -166,6 +170,7 @@ function MetadataSidebarRedesign({ }; const handleCancel = () => { + clearExtractError(); setEditingTemplate(null); }; @@ -189,6 +194,7 @@ function MetadataSidebarRedesign({ } catch { // ignore error, handled in useSidebarMetadataFetcher } + clearExtractError(); setEditingTemplate(null); }; @@ -279,6 +285,7 @@ function MetadataSidebarRedesign({ {editingTemplate && ( { beforeEach(() => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -127,6 +128,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); }); @@ -142,6 +144,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should have accessible "All templates" combobox trigger button', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -151,6 +154,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -184,6 +188,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should have accessible "All templates" combobox trigger button', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -193,6 +198,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -204,6 +210,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata sidebar with error', async () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -216,6 +223,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { }, status: STATUS.ERROR, file: mockFile, + extractErrorCode: null, }); const errorMessage = { id: 'error', defaultMessage: 'error message' }; @@ -227,6 +235,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata sidebar with loading indicator', async () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -236,6 +245,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.LOADING, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -266,6 +276,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render empty state when no visible template instances are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -275,6 +286,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -288,6 +300,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata instance list when templates are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -297,6 +310,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -311,6 +325,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render filter dropdown when more than one templates are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -320,6 +335,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -337,6 +353,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { 'should not render filter dropdown when only one or none visible template is present', (templateInstances: MetadataTemplateInstance[]) => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -346,6 +363,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); renderComponent(); @@ -356,6 +374,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata filterd instance list when fileterd templates are present and matching', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -365,6 +384,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); const filteredTemplateIds = [mockVisibleTemplateInstance.id]; @@ -378,6 +398,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { test('should render metadata unfiltered instance list when fileterd templates are present and do not match existing templates', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ + clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), @@ -387,6 +408,7 @@ describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { errorMessage: null, status: STATUS.SUCCESS, file: mockFile, + extractErrorCode: null, }); const filteredTemplateIds = ['non-existing-template-id']; diff --git a/src/elements/content-sidebar/__tests__/useSidebarMetadataFetcher.test.tsx b/src/elements/content-sidebar/__tests__/useSidebarMetadataFetcher.test.tsx index dfd89c5cc2..9a006567d4 100644 --- a/src/elements/content-sidebar/__tests__/useSidebarMetadataFetcher.test.tsx +++ b/src/elements/content-sidebar/__tests__/useSidebarMetadataFetcher.test.tsx @@ -4,17 +4,35 @@ import messages from '../../common/messages'; import { ERROR_CODE_EMPTY_METADATA_SUGGESTIONS, ERROR_CODE_FETCH_METADATA_SUGGESTIONS, + ERROR_CODE_METADATA_AUTOFILL_TIMEOUT, + ERROR_CODE_METADATA_PRECONDITION_FAILED, + ERROR_CODE_UNKNOWN, FIELD_PERMISSIONS_CAN_UPLOAD, SUCCESS_CODE_DELETE_METADATA_TEMPLATE_INSTANCE, SUCCESS_CODE_UPDATE_METADATA_TEMPLATE_INSTANCE, } from '../../../constants'; import useSidebarMetadataFetcher, { STATUS } from '../hooks/useSidebarMetadataFetcher'; -const mockError = { +const mockRateLimitError = { + status: 429, + message: 'Rate Limit Exceeded', +}; + +const mockInternalServerError = { status: 500, message: 'Internal Server Error', }; +const mockTimeoutError = { + status: 408, + message: 'Request Timeout', +}; + +const mockPreconditionFailedError = { + status: 412, + message: 'Precondition Failed', +}; + const mockFile = { id: '123', permissions: { [FIELD_PERMISSIONS_CAN_UPLOAD]: true }, @@ -147,7 +165,7 @@ describe('useSidebarMetadataFetcher', () => { test('should handle file fetching error', async () => { mockAPI.getFile.mockImplementation((id, successCallback, errorCallback) => - errorCallback(mockError, 'file_fetch_error'), + errorCallback(mockInternalServerError, 'file_fetch_error'), ); const { result } = setupHook(); @@ -158,10 +176,10 @@ describe('useSidebarMetadataFetcher', () => { expect(result.current.errorMessage).toBe(messages.sidebarMetadataEditingErrorContent); expect(onSuccessMock).not.toHaveBeenCalled(); expect(onErrorMock).toHaveBeenCalledWith( - mockError, + mockInternalServerError, 'file_fetch_error', expect.objectContaining({ - error: mockError, + error: mockInternalServerError, isErrorDisplayed: true, }), ); @@ -172,7 +190,7 @@ describe('useSidebarMetadataFetcher', () => { successCallback(mockFile); }); mockAPI.getMetadata.mockImplementation((file, successCallback, errorCallback) => { - errorCallback(mockError, 'metadata_fetch_error'); + errorCallback(mockInternalServerError, 'metadata_fetch_error'); }); const { result } = setupHook(); @@ -182,10 +200,10 @@ describe('useSidebarMetadataFetcher', () => { expect(result.current.errorMessage).toBe(messages.sidebarMetadataFetchingErrorContent); expect(onSuccessMock).not.toHaveBeenCalled(); expect(onErrorMock).toHaveBeenCalledWith( - mockError, + mockInternalServerError, 'metadata_fetch_error', expect.objectContaining({ - error: mockError, + error: mockInternalServerError, isErrorDisplayed: true, }), ); @@ -215,7 +233,7 @@ describe('useSidebarMetadataFetcher', () => { successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates }); }); mockAPI.deleteMetadata.mockImplementation((file, template, successCallback, errorCallback) => { - errorCallback(mockError, 'metadata_remove_error'); + errorCallback(mockInternalServerError, 'metadata_remove_error'); }); const { result } = setupHook(); @@ -226,10 +244,10 @@ describe('useSidebarMetadataFetcher', () => { expect(result.current.status).toEqual(STATUS.ERROR); expect(onSuccessMock).not.toHaveBeenCalled(); expect(onErrorMock).toHaveBeenCalledWith( - mockError, + mockInternalServerError, 'metadata_remove_error', expect.objectContaining({ - error: mockError, + error: mockInternalServerError, isErrorDisplayed: true, }), ); @@ -259,7 +277,7 @@ describe('useSidebarMetadataFetcher', () => { successCallback({ templateInstances: mockTemplateInstances, templates: mockTemplates }); }); mockAPI.createMetadataRedesign.mockImplementation((file, template, successCallback, errorCallback) => { - errorCallback(mockError, 'metadata_creation_error'); + errorCallback(mockInternalServerError, 'metadata_creation_error'); }); const { result } = setupHook(); @@ -270,10 +288,10 @@ describe('useSidebarMetadataFetcher', () => { expect(result.current.status).toBe(STATUS.ERROR); expect(onSuccessMock).not.toHaveBeenCalled(); expect(onErrorMock).toHaveBeenCalledWith( - mockError, + mockInternalServerError, 'metadata_creation_error', expect.objectContaining({ - error: mockError, + error: mockInternalServerError, isErrorDisplayed: true, }), ); @@ -302,7 +320,7 @@ describe('useSidebarMetadataFetcher', () => { test('should handle metadata update error', async () => { mockAPI.updateMetadataRedesign.mockImplementation( (_file, _metadataInstance, _JSONPatch, successCallback, errorCallback) => { - errorCallback(mockError, 'metadata_update_error'); + errorCallback(mockInternalServerError, 'metadata_update_error'); }, ); const ops = [{ op: 'add', path: '/foo', value: 'bar' }]; @@ -322,10 +340,10 @@ describe('useSidebarMetadataFetcher', () => { expect(result.current.templates).toEqual(mockTemplates); expect(result.current.errorMessage).toEqual(messages.sidebarMetadataEditingErrorContent); expect(onErrorMock).toHaveBeenCalledWith( - mockError, + mockInternalServerError, 'metadata_update_error', expect.objectContaining({ - error: mockError, + error: mockInternalServerError, isErrorDisplayed: true, }), ); @@ -351,8 +369,8 @@ describe('useSidebarMetadataFetcher', () => { ]); }); - test('should handle error during suggestions extraction', async () => { - mockAPI.extractStructured.mockRejectedValue(mockError); + test('should handle user correctable error during suggestions extraction', async () => { + mockAPI.extractStructured.mockRejectedValue({ response: mockRateLimitError }); const { result } = setupHook(); const suggestions = await result.current.extractSuggestions('templateKey', 'global'); @@ -360,12 +378,33 @@ describe('useSidebarMetadataFetcher', () => { expect(suggestions).toEqual([]); expect(onSuccessMock).not.toHaveBeenCalled(); expect(onErrorMock).toHaveBeenCalledWith( - mockError, + { response: mockRateLimitError }, ERROR_CODE_FETCH_METADATA_SUGGESTIONS, expect.objectContaining({ showNotification: true, }), ); + await waitFor(() => expect(result.current.extractErrorCode).toBeNull()); + }); + + test.each` + description | error | expectedErrorCode + ${'metadata autofill timeout error'} | ${{ response: mockTimeoutError }} | ${ERROR_CODE_METADATA_AUTOFILL_TIMEOUT} + ${'metadata pre-condition failure error'} | ${{ response: mockPreconditionFailedError }} | ${ERROR_CODE_METADATA_PRECONDITION_FAILED} + ${'internal server error'} | ${{ response: mockInternalServerError }} | ${ERROR_CODE_UNKNOWN} + `('should set extract error code for $description and get cleared', async ({ error, expectedErrorCode }) => { + mockAPI.extractStructured.mockRejectedValue(error); + + const { result } = setupHook(); + const suggestions = await result.current.extractSuggestions('templateKey', 'global'); + + expect(suggestions).toEqual([]); + expect(onSuccessMock).not.toHaveBeenCalled(); + expect(onErrorMock).toHaveBeenCalledWith(error, expectedErrorCode); + await waitFor(() => expect(result.current.extractErrorCode).toEqual(expectedErrorCode)); + + await result.current.clearExtractError(); + await waitFor(() => expect(result.current.extractErrorCode).toBeNull()); }); test('should handle empty suggestions', async () => { diff --git a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts index 3c11c1265c..b75bb8893c 100644 --- a/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts +++ b/src/elements/content-sidebar/hooks/useSidebarMetadataFetcher.ts @@ -15,6 +15,9 @@ import { isUserCorrectableError } from '../../../utils/error'; import { ERROR_CODE_EMPTY_METADATA_SUGGESTIONS, ERROR_CODE_FETCH_METADATA_SUGGESTIONS, + ERROR_CODE_METADATA_AUTOFILL_TIMEOUT, + ERROR_CODE_UNKNOWN, + ERROR_CODE_METADATA_PRECONDITION_FAILED, FIELD_IS_EXTERNALLY_OWNED, FIELD_PERMISSIONS_CAN_UPLOAD, FIELD_PERMISSIONS, @@ -35,7 +38,13 @@ export enum STATUS { } interface DataFetcher { + clearExtractError: () => void; errorMessage: MessageDescriptor | null; + extractErrorCode: + | ERROR_CODE_METADATA_AUTOFILL_TIMEOUT + | ERROR_CODE_METADATA_PRECONDITION_FAILED + | ERROR_CODE_UNKNOWN + | null; extractSuggestions: (templateKey: string, scope: string) => Promise; file: BoxItem | null; handleCreateMetadataInstance: ( @@ -65,6 +74,7 @@ function useSidebarMetadataFetcher( const [templates, setTemplates] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(null); const [templateInstances, setTemplateInstances] = React.useState>([]); + const [extractErrorCode, setExtractErrorCode] = React.useState(null); const onApiError = React.useCallback( (error: ElementsXhrError, code: string, message: MessageDescriptor) => { @@ -206,6 +216,7 @@ function useSidebarMetadataFetcher( const extractSuggestions = React.useCallback( async (templateKey: string, scope: string): Promise => { const aiAPI = api.getIntelligenceAPI(); + setExtractErrorCode(null); let answer = null; try { @@ -214,15 +225,25 @@ function useSidebarMetadataFetcher( metadata_template: { template_key: templateKey, scope, type: 'metadata_template' }, })) as Record; } catch (error) { - if (isUserCorrectableError(error.status)) { + // Axios makes the status code nested under the response object + if (error.response?.status === 408) { + onError(error, ERROR_CODE_METADATA_AUTOFILL_TIMEOUT); + setExtractErrorCode(ERROR_CODE_METADATA_AUTOFILL_TIMEOUT); + } else if (error.response?.status === 412) { + onError(error, ERROR_CODE_METADATA_PRECONDITION_FAILED); + setExtractErrorCode(ERROR_CODE_METADATA_PRECONDITION_FAILED); + } else if (error.response?.status === 500) { + onError(error, ERROR_CODE_UNKNOWN); + setExtractErrorCode(ERROR_CODE_UNKNOWN); + } else if (isUserCorrectableError(error.response?.status)) { onError(error, ERROR_CODE_FETCH_METADATA_SUGGESTIONS, { showNotification: true }); } else { + onError(error, ERROR_CODE_UNKNOWN, { showNotification: true }); // react way of throwing errors from async callbacks - https://github.com/facebook/react/issues/14981#issuecomment-468460187 setError(() => { throw error; }); } - return []; } @@ -261,10 +282,12 @@ function useSidebarMetadataFetcher( }, [api, fetchFileErrorCallback, fetchFileSuccessCallback, fileId, status]); return { + clearExtractError: () => setExtractErrorCode(null), extractSuggestions, handleCreateMetadataInstance, handleDeleteMetadataInstance, handleUpdateMetadataInstance, + extractErrorCode, errorMessage, file, status, 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 e54c949224..c51bdb5084 100644 --- a/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx +++ b/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx @@ -432,7 +432,7 @@ export const ShowErrorWhenAIAPIIsUnavailable: StoryObj { - return new HttpResponse('Internal Server Error', { status: 500 }); + return new HttpResponse('Not Found', { status: 404 }); }), ], }, diff --git a/yarn.lock b/yarn.lock index f49cef3833..6c73f80ac7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1528,10 +1528,10 @@ resolved "https://registry.yarnpkg.com/@box/languages/-/languages-1.1.2.tgz#cd4266b3da62da18560d881e10b429653186be29" integrity sha512-d64TGosx+KRmrLZj4CIyLp42LUiEbgBJ8n8cviMQwTJmfU0g+UwZqLjmQZR1j+Q9D64yV4xHzY9K1t5nInWWeQ== -"@box/metadata-editor@^0.88.1": - version "0.88.1" - resolved "https://registry.yarnpkg.com/@box/metadata-editor/-/metadata-editor-0.88.1.tgz#6beedb9e7c73cdd8824f9f52e641ac1ff1899967" - integrity sha512-Q6GXv9B0wSbDh9Uy7tLOg9lMeBiNx3d2WYoEZsf8Jm9kC2w8dWIGbHRqiD3E6GpvomhSrWfHzt2Zy/Bn7BRXDg== +"@box/metadata-editor@0.91.0": + version "0.91.0" + resolved "https://registry.yarnpkg.com/@box/metadata-editor/-/metadata-editor-0.91.0.tgz#f7562f4cefd224aad491e955adefce2147c2a803" + integrity sha512-TyJD6n8jS5MW9rericaLlG7h203+xWpjt9deqkrYiQ9K6Vu79FqAmrW3sbBRHJFREy6Os34SruvSqL71qCPclw== "@box/react-virtualized@9.22.3-rc-box.9": version "9.22.3-rc-box.9"