diff --git a/packages/compass-crud/src/components/bulk-actions-toasts.spec.tsx b/packages/compass-crud/src/components/bulk-actions-toasts.spec.tsx index 6d9f53d989c..9f821da670d 100644 --- a/packages/compass-crud/src/components/bulk-actions-toasts.spec.tsx +++ b/packages/compass-crud/src/components/bulk-actions-toasts.spec.tsx @@ -17,6 +17,7 @@ import { } from './bulk-actions-toasts'; import { expect } from 'chai'; import sinon from 'sinon'; +import { MongoNetworkError } from 'mongodb'; function renderToastPortal() { return render(); @@ -67,17 +68,29 @@ describe('Bulk Action Toasts', function () { { modal: openBulkDeleteFailureToast, affected: undefined, - expected: 'The delete operation failed.', + error: new Error('Test error'), + expected: ['The delete operation failed.', 'Test error'], }, { modal: openBulkDeleteFailureToast, affected: 1, - expected: '1 document could not been deleted.', + error: new Error('Another test error'), + expected: ['1 document could not be deleted.', 'Another test error'], }, { modal: openBulkDeleteFailureToast, affected: 2, - expected: '2 documents could not been deleted.', + error: new Error('Another failure'), + expected: ['2 documents could not be deleted.', 'Another failure'], + }, + { + modal: openBulkDeleteFailureToast, + affected: 2, + error: new MongoNetworkError('Connection lost'), + expected: [ + 'Delete operation - network error occurred.', + 'Connection lost', + ], }, ]; @@ -85,12 +98,18 @@ describe('Bulk Action Toasts', function () { it(`${useCase.modal.name} shows the text '${useCase.expected}' when affected document/s is/are '${useCase.affected}'`, async function () { useCase.modal({ affectedDocuments: useCase.affected, + error: useCase.error as Error, onRefresh: () => {}, }); await waitFor(async function () { - const node = await screen.findByText(useCase.expected); - expect(node).to.exist; + if (!Array.isArray(useCase.expected)) { + expect(await screen.findByText(useCase.expected)).to.exist; + } else { + for (const expectedText of useCase.expected) { + expect(await screen.findByText(expectedText)).to.exist; + } + } }); }); } @@ -160,17 +179,30 @@ describe('Bulk Action Toasts', function () { { modal: openBulkUpdateFailureToast, affected: undefined, - expected: 'The update operation failed.', + error: new Error('Test error'), + expected: ['The update operation failed.', 'Test error'], }, { modal: openBulkUpdateFailureToast, affected: 1, - expected: '1 document could not been updated.', + error: new Error('Could not update'), + expected: ['1 document could not be updated.', 'Could not update'], }, { modal: openBulkUpdateFailureToast, affected: 2, - expected: '2 documents could not been updated.', + error: new Error('Update failed'), + expected: ['2 documents could not be updated.', 'Update failed'], + }, + + { + modal: openBulkUpdateFailureToast, + affected: 2, + error: new MongoNetworkError('Connection lost'), + expected: [ + 'Update operation - network error occurred.', + 'Connection lost', + ], }, ]; @@ -178,12 +210,18 @@ describe('Bulk Action Toasts', function () { it(`${useCase.modal.name} shows the text '${useCase.expected}' when ${useCase.affected} document/s affected`, async function () { useCase.modal({ affectedDocuments: useCase.affected, + error: useCase.error, onRefresh: () => {}, }); await waitFor(async function () { - const node = await screen.findByText(useCase.expected); - expect(node).to.exist; + if (!Array.isArray(useCase.expected)) { + expect(await screen.findByText(useCase.expected)).to.exist; + } else { + for (const expectedText of useCase.expected) { + expect(await screen.findByText(expectedText)).to.exist; + } + } }); }); } diff --git a/packages/compass-crud/src/components/bulk-actions-toasts.tsx b/packages/compass-crud/src/components/bulk-actions-toasts.tsx index 474470ba8a3..be1c308072f 100644 --- a/packages/compass-crud/src/components/bulk-actions-toasts.tsx +++ b/packages/compass-crud/src/components/bulk-actions-toasts.tsx @@ -4,6 +4,7 @@ import { closeToast, ToastBody, } from '@mongodb-js/compass-components'; +import { MongoNetworkError } from 'mongodb'; type BulkDeleteSuccessToastProps = { affectedDocuments?: number; @@ -72,33 +73,48 @@ export function openBulkDeleteProgressToast({ }); } -type BulkDeleteFailureToastProps = { +type BulkOperationFailureToastProps = { affectedDocuments?: number; + error: Error; + type: 'delete' | 'update'; }; -export function openBulkDeleteFailureToast({ +const isNetworkError = (error: Error) => error instanceof MongoNetworkError; + +export function openBulkOperationFailureToast({ affectedDocuments, -}: BulkDeleteFailureToastProps): void { - let text; - switch (affectedDocuments) { - case undefined: - text = 'The delete operation failed.'; - break; - case 1: - text = `${affectedDocuments} document could not been deleted.`; - break; - default: - text = `${affectedDocuments} documents could not been deleted.`; + error, + type, +}: BulkOperationFailureToastProps): void { + let title: string; + if (isNetworkError(error)) { + title = `${ + type === 'delete' ? 'Delete' : 'Update' + } operation - network error occurred.`; + } else if (affectedDocuments === undefined) { + title = `The ${type} operation failed.`; + } else if (affectedDocuments === 1) { + title = `${affectedDocuments} document could not be ${ + type === 'delete' ? 'deleted' : 'updated' + }.`; + } else { + title = `${affectedDocuments} documents could not be ${ + type === 'delete' ? 'deleted' : 'updated' + }.`; } - openToast('bulk-delete-toast', { - title: '', + openToast(`bulk-${type}-toast`, { + title, variant: 'warning', dismissible: true, - description: , + description: , }); } +export const openBulkDeleteFailureToast = ( + props: Omit +): void => openBulkOperationFailureToast({ ...props, type: 'delete' }); + type BulkUpdateSuccessToastProps = { affectedDocuments?: number; onRefresh: () => void; @@ -166,29 +182,6 @@ export function openBulkUpdateProgressToast({ }); } -type BulkUpdateFailureToastProps = { - affectedDocuments?: number; -}; - -export function openBulkUpdateFailureToast({ - affectedDocuments, -}: BulkUpdateFailureToastProps): void { - let text; - switch (affectedDocuments) { - case undefined: - text = 'The update operation failed.'; - break; - case 1: - text = `${affectedDocuments} document could not been updated.`; - break; - default: - text = `${affectedDocuments} documents could not been updated.`; - } - - openToast('bulk-update-toast', { - title: '', - variant: 'warning', - dismissible: true, - description: , - }); -} +export const openBulkUpdateFailureToast = ( + props: Omit +): void => openBulkOperationFailureToast({ ...props, type: 'update' }); diff --git a/packages/compass-crud/src/stores/crud-store.ts b/packages/compass-crud/src/stores/crud-store.ts index 81bc2087c3d..94266d6a916 100644 --- a/packages/compass-crud/src/stores/crud-store.ts +++ b/packages/compass-crud/src/stores/crud-store.ts @@ -2,7 +2,6 @@ import type { Listenable, Store } from 'reflux'; import Reflux from 'reflux'; import toNS from 'mongodb-ns'; import { findIndex, isEmpty, isEqual } from 'lodash'; -import type { MongoServerError } from 'mongodb'; import semver from 'semver'; import StateMixin from '@mongodb-js/reflux-state-mixin'; import type { Element } from 'hadron-document'; @@ -69,6 +68,7 @@ import type { } from '@mongodb-js/compass-connections/provider'; import type { Query, QueryBarService } from '@mongodb-js/compass-query-bar'; import type { TrackFunction } from '@mongodb-js/compass-telemetry'; +import type { MongoServerError } from 'mongodb'; export type BSONObject = TypeCastMap['Object']; export type BSONArray = TypeCastMap['Array']; @@ -1243,6 +1243,7 @@ class CrudStoreImpl } catch (err: any) { openBulkUpdateFailureToast({ affectedDocuments: this.state.bulkUpdate.affected, + error: err as Error, }); this.logger.log.error( @@ -1899,6 +1900,7 @@ class CrudStoreImpl bulkDeleteFailed(ex: Error) { openBulkDeleteFailureToast({ affectedDocuments: this.state.bulkDelete.affected, + error: ex, }); this.logger.log.error(