diff --git a/src/course-libraries/ReviewTabContent.tsx b/src/course-libraries/ReviewTabContent.tsx index 1d613e098..63f02cef0 100644 --- a/src/course-libraries/ReviewTabContent.tsx +++ b/src/course-libraries/ReviewTabContent.tsx @@ -144,7 +144,7 @@ const ItemReviewList = ({ const { hits, - isLoading: isIndexDataLoading, + isPending: isIndexDataPending, hasError, hasNextPage, isFetchingNextPage, @@ -243,7 +243,7 @@ const ItemReviewList = ({ } }, [blockData]); - if (isIndexDataLoading) { + if (isIndexDataPending) { return ; } diff --git a/src/library-authoring/LibraryContent.test.tsx b/src/library-authoring/LibraryContent.test.tsx index 810c77f48..8e12757ac 100644 --- a/src/library-authoring/LibraryContent.test.tsx +++ b/src/library-authoring/LibraryContent.test.tsx @@ -1,12 +1,13 @@ import fetchMock from 'fetch-mock-jest'; +import { getContentSearchConfigUrl } from '@src/search-manager/data/api'; import { fireEvent, render, screen, initializeMocks, -} from '../testUtils'; -import { getContentSearchConfigUrl } from '../search-manager/data/api'; +} from '@src/testUtils'; + import { mockContentLibrary } from './data/api.mocks'; import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; import { LibraryProvider } from './common/context/LibraryContext'; @@ -27,7 +28,7 @@ const data = { fetchNextPage: mockFetchNextPage, searchKeywords: '', isFiltered: false, - isLoading: false, + isPending: false, }; const returnEmptyResult = (_url: string, req) => { @@ -77,7 +78,7 @@ describe('', () => { it('should render a spinner while loading', async () => { mockUseSearchContext.mockReturnValue({ ...data, - isLoading: true, + isPending: true, }); render(, withLibraryId(mockContentLibrary.libraryId)); diff --git a/src/library-authoring/LibraryContent.tsx b/src/library-authoring/LibraryContent.tsx index 817c1d46d..cb381f597 100644 --- a/src/library-authoring/LibraryContent.tsx +++ b/src/library-authoring/LibraryContent.tsx @@ -36,7 +36,7 @@ const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps) isFetchingNextPage, hasNextPage, fetchNextPage, - isLoading, + isPending, isFiltered, usageKey, } = useSearchContext(); @@ -56,7 +56,7 @@ const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps) true, ); - if (isLoading) { + if (isPending) { return ; } if (totalHits === 0) { diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx index 27803a774..9681bb4b1 100644 --- a/src/library-authoring/component-info/ComponentUsage.tsx +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -35,7 +35,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { data: dataDownstreamLinks, isError: isErrorDownstreamLinks, error: errorDownstreamLinks, - isLoading: isLoadingDownstreamLinks, + isPending: isPendingDownstreamLinks, } = useEntityLinks({ upstreamKey: usageKey, contentType: 'components' }); const downstreamKeys = useMemo( @@ -47,14 +47,14 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { hits: downstreamHits, isError: isErrorIndexDocuments, error: errorIndexDocuments, - isLoading: isLoadingIndexDocuments, + isPending: isPendingIndexDocuments, } = useContentFromSearchIndex(downstreamKeys); if (isErrorDownstreamLinks || isErrorIndexDocuments) { return ; } - if (isLoadingDownstreamLinks || (isLoadingIndexDocuments && !!downstreamKeys.length)) { + if (isPendingDownstreamLinks || (isPendingIndexDocuments && !!downstreamKeys.length)) { return ; } diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index 4b1f46081..95f4316a9 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -7,17 +7,17 @@ import { IconButton, useToggle, } from '@openedx/paragon'; -import { MoreVert } from '@openedx/paragon/icons'; +import { Delete, MoreVert } from '@openedx/paragon/icons'; -import { type CollectionHit } from '../../search-manager'; +import DeleteModal from '@src/generic/delete-modal/DeleteModal'; +import { ToastContext } from '@src/generic/toast-context'; +import { type CollectionHit } from '@src/search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; import { SidebarBodyItemId, useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryRoutes } from '../routes'; import BaseCard from './BaseCard'; -import { ToastContext } from '../../generic/toast-context'; import { useDeleteCollection, useRestoreCollection } from '../data/apiHooks'; -import DeleteModal from '../../generic/delete-modal/DeleteModal'; import messages from './messages'; type CollectionMenuProps = { @@ -98,7 +98,8 @@ const CollectionMenu = ({ hit } : CollectionMenuProps) => { loads data mockLibraryBlockMetadata.applyMock(); @@ -41,28 +41,16 @@ describe('', () => { beforeEach(() => { const mocks = initializeMocks(); mockShowToast = mocks.mockShowToast; - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [], - }, - ], - }); - }); - - it('is invisible when isConfirmingDelete is false', async () => { - const mockCancel = jest.fn(); - render(, renderArgs); - - const modal = screen.queryByRole('dialog', { name: 'Delete Component' }); - expect(modal).not.toBeInTheDocument(); + mockSearchResult(hydrateSearchResult([{ + displayName: 'Introduction to Testing 2', + }])); }); it('should shows a confirmation prompt the card with title and description', async () => { const mockCancel = jest.fn(); - render(, renderArgs); + render(, renderArgs); - const modal = screen.getByRole('dialog', { name: 'Delete Component' }); + const modal = await screen.findByRole('dialog', { name: 'Delete Component' }); expect(modal).toBeVisible(); // It should mention the component's name in the confirm dialog: @@ -76,9 +64,9 @@ describe('', () => { it('deletes the block when confirmed, shows a toast with undo option and restores block on undo', async () => { const mockCancel = jest.fn(); - render(, renderArgs); + render(, renderArgs); - const modal = screen.getByRole('dialog', { name: 'Delete Component' }); + const modal = await screen.findByRole('dialog', { name: 'Delete Component' }); expect(modal).toBeVisible(); const deleteButton = screen.getByRole('button', { name: 'Delete' }); @@ -99,23 +87,17 @@ describe('', () => { it('should show units message if `unitsData` is set with one unit', async () => { const mockCancel = jest.fn(); - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [{ - units: { - displayName: ['Unit 1'], - key: ['unit1'], - }, - }], - }, - ], - }); + mockSearchResult(hydrateSearchResult([{ + displayName: 'Introduction to Testing 2', + units: { + displayName: ['Unit 1'], + key: ['unit1'], + }, + }])); render( , renderArgs, ); @@ -131,23 +113,17 @@ describe('', () => { it('should show units message if `unitsData` is set with multiple units', async () => { const mockCancel = jest.fn(); - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [{ - units: { - displayName: ['Unit 1', 'Unit 2'], - key: ['unit1', 'unit2'], - }, - }], - }, - ], - }); + mockSearchResult(hydrateSearchResult([{ + displayName: 'Introduction to Testing 2', + units: { + displayName: ['Unit 1', 'Unit 2'], + key: ['unit1', 'unit2'], + }, + }])); render( , renderArgs, ); diff --git a/src/library-authoring/components/ComponentDeleter.tsx b/src/library-authoring/components/ComponentDeleter.tsx index 13a48a7f0..48b8e5e60 100644 --- a/src/library-authoring/components/ComponentDeleter.tsx +++ b/src/library-authoring/components/ComponentDeleter.tsx @@ -1,48 +1,37 @@ import React, { useCallback, useContext } from 'react'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Icon } from '@openedx/paragon'; -import { School, Warning, Info } from '@openedx/paragon/icons'; +import { School, Delete, Info } from '@openedx/paragon/icons'; +import { useEntityLinks } from '@src/course-libraries/data/apiHooks'; +import DeleteModal from '@src/generic/delete-modal/DeleteModal'; +import { ToastContext } from '@src/generic/toast-context'; +import { type ContentHit } from '@src/search-manager'; import { useSidebarContext } from '../common/context/SidebarContext'; +import { useLibraryContext } from '../common/context/LibraryContext'; import { useContentFromSearchIndex, useDeleteLibraryBlock, - useLibraryBlockMetadata, useRestoreLibraryBlock, } from '../data/apiHooks'; import messages from './messages'; -import { ToastContext } from '../../generic/toast-context'; -import DeleteModal from '../../generic/delete-modal/DeleteModal'; -import { type ContentHit } from '../../search-manager'; - -/** - * Helper component to load and display the name of the block. - * - * This needs to be a separate component so that we only query the metadata of - * the block when needed (when this is displayed), not on every card shown in - * the search results. - */ -const BlockName = (props: { usageKey: string }) => { - const { data: blockMetadata } = useLibraryBlockMetadata(props.usageKey); - - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>{blockMetadata?.displayName} ?? ; -}; interface Props { usageKey: string; - /** If true, show a confirmation modal that asks the user if they want to delete this component. */ - isConfirmingDelete: boolean; - cancelDelete: () => void; + close: () => void; } -const ComponentDeleter = ({ usageKey, ...props }: Props) => { +const ComponentDeleter = ({ usageKey, close }: Props) => { const intl = useIntl(); const { sidebarItemInfo, closeLibrarySidebar } = useSidebarContext(); const { showToast } = useContext(ToastContext); + const { containerId: currentUnitId } = useLibraryContext(); const sidebarComponentUsageKey = sidebarItemInfo?.id; const restoreComponentMutation = useRestoreLibraryBlock(); + const { data: dataDownstreamLinks, isPending: isPendingLinks } = useEntityLinks({ upstreamKey: usageKey, contentType: 'components' }); + const downstreamCount = dataDownstreamLinks?.length ?? 0; + const restoreComponent = useCallback(async () => { try { await restoreComponentMutation.mutateAsync({ usageKey }); @@ -62,40 +51,40 @@ const ComponentDeleter = ({ usageKey, ...props }: Props) => { onClick: restoreComponent, }, ); - props.cancelDelete(); + close(); // Close the sidebar if it's still open showing the deleted component: if (usageKey === sidebarComponentUsageKey) { closeLibrarySidebar(); } }, [usageKey, sidebarComponentUsageKey, closeLibrarySidebar]); - const { hits } = useContentFromSearchIndex([usageKey]); + const { hits, isPending } = useContentFromSearchIndex([usageKey]); const componentHit = (hits as ContentHit[])?.[0]; - if (!props.isConfirmingDelete) { + // istanbul ignore if: loading state + if (isPending || isPendingLinks) { + // Only render the modal after loading return null; } - let unitsMessage; - const unitsLength = componentHit?.units?.displayName?.length ?? 0; - if (unitsLength === 1) { - unitsMessage = componentHit?.units?.displayName?.[0]; - } else if (unitsLength > 1) { - unitsMessage = `${unitsLength} units`; + const currentUnitIndex = componentHit?.units?.key?.findIndex((id) => id === currentUnitId); + const otherUnits = componentHit?.units?.displayName?.filter( + (_, index) => index !== currentUnitIndex, + ); + let unitsMessage: string | undefined; + const otherUnitsLength = otherUnits?.length ?? 0; + if (otherUnitsLength === 1) { + unitsMessage = otherUnits?.[0]; + } else if (otherUnitsLength > 1) { + unitsMessage = `${otherUnitsLength} Units`; } const deleteText = intl.formatMessage(messages.deleteComponentConfirm, { - componentName: , + componentName: {componentHit?.displayName}, message: ( - <> -
- - {unitsMessage - ? intl.formatMessage(messages.deleteComponentConfirmCourseSmall) - : intl.formatMessage(messages.deleteComponentConfirmCourse)} -
+
{unitsMessage && ( -
+
{
)} - + {(downstreamCount || 0) > 0 && ( +
+ + + {intl.formatMessage(messages.deleteComponentConfirmCourse, { + courseCount: downstreamCount, + courseCountText: {downstreamCount}, + })} + +
+ )} +
), }); return ( diff --git a/src/library-authoring/components/ComponentMenu.tsx b/src/library-authoring/components/ComponentMenu.tsx index eeaa5ba22..1a46ce50f 100644 --- a/src/library-authoring/components/ComponentMenu.tsx +++ b/src/library-authoring/components/ComponentMenu.tsx @@ -8,18 +8,16 @@ import { } from '@openedx/paragon'; import { MoreVert } from '@openedx/paragon/icons'; +import { useClipboard } from '@src/generic/clipboard'; import { getBlockType } from '@src/generic/key-utils'; +import { ToastContext } from '@src/generic/toast-context'; + import { useLibraryContext } from '../common/context/LibraryContext'; import { SidebarActions, SidebarBodyItemId, useSidebarContext } from '../common/context/SidebarContext'; -import { useClipboard } from '../../generic/clipboard'; -import { ToastContext } from '../../generic/toast-context'; -import { - useAddItemsToContainer, - useRemoveContainerChildren, - useRemoveItemsFromCollection, -} from '../data/apiHooks'; +import { useRemoveItemsFromCollection } from '../data/apiHooks'; import { canEditComponent } from './ComponentEditorModal'; import ComponentDeleter from './ComponentDeleter'; +import ComponentRemover from './ComponentRemover'; import messages from './messages'; import containerMessages from '../containers/messages'; import { useLibraryRoutes } from '../routes'; @@ -44,10 +42,9 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const canEdit = usageKey && canEditComponent(usageKey); const { showToast } = useContext(ToastContext); - const addItemToContainerMutation = useAddItemsToContainer(containerId); const removeCollectionComponentsMutation = useRemoveItemsFromCollection(libraryId, collectionId); - const removeContainerItemMutation = useRemoveContainerChildren(containerId); - const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false); + const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); + const [isRemoveModalOpen, openRemoveModal, closeRemoveModal] = useToggle(false); const { copyToClipboard } = useClipboard(); const updateClipboardClick = () => { @@ -66,32 +63,6 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { }); }; - const removeFromContainer = () => { - const restoreComponent = () => { - addItemToContainerMutation.mutateAsync([usageKey]).then(() => { - showToast(intl.formatMessage(messages.undoRemoveComponentFromContainerToastSuccess)); - }).catch(() => { - showToast(intl.formatMessage(messages.undoRemoveComponentFromContainerToastFailed)); - }); - }; - - removeContainerItemMutation.mutateAsync([usageKey]).then(() => { - if (sidebarItemInfo?.id === usageKey) { - // Close sidebar if current component is open - closeLibrarySidebar(); - } - showToast( - intl.formatMessage(messages.removeComponentFromContainerSuccess), - { - label: intl.formatMessage(messages.undoRemoveComponentFromContainerToastAction), - onClick: restoreComponent, - }, - ); - }).catch(() => { - showToast(intl.formatMessage(messages.removeComponentFromContainerFailure)); - }); - }; - const handleEdit = useCallback(() => { openItemSidebar(usageKey, SidebarBodyItemId.ComponentInfo); openComponentEditor(usageKey); @@ -134,11 +105,11 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { {containerId && ( - + )} - + {insideCollection && ( @@ -155,11 +126,16 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { - {isConfirmingDelete && ( + {isDeleteModalOpen && ( + )} + {isRemoveModalOpen && ( + )} diff --git a/src/library-authoring/components/ComponentRemover.tsx b/src/library-authoring/components/ComponentRemover.tsx new file mode 100644 index 000000000..1f3a1ca6d --- /dev/null +++ b/src/library-authoring/components/ComponentRemover.tsx @@ -0,0 +1,86 @@ +import { useContext } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Warning } from '@openedx/paragon/icons'; + +import DeleteModal from '@src/generic/delete-modal/DeleteModal'; +import { ToastContext } from '@src/generic/toast-context'; +import { useLibraryContext } from '../common/context/LibraryContext'; +import { useSidebarContext } from '../common/context/SidebarContext'; +import { + useContainer, + useRemoveContainerChildren, + useAddItemsToContainer, + useLibraryBlockMetadata, +} from '../data/apiHooks'; +import messages from './messages'; + +interface Props { + usageKey: string; + close: () => void; +} + +const ComponentRemover = ({ usageKey, close }: Props) => { + const intl = useIntl(); + const { sidebarItemInfo, closeLibrarySidebar } = useSidebarContext(); + const { containerId } = useLibraryContext(); + const { showToast } = useContext(ToastContext); + + const removeContainerItemMutation = useRemoveContainerChildren(containerId); + const addItemToContainerMutation = useAddItemsToContainer(containerId); + const { data: container, isPending: isPendingParentContainer } = useContainer(containerId); + const { data: component, isPending } = useLibraryBlockMetadata(usageKey); + + // istanbul ignore if: loading state + if (isPending || isPendingParentContainer) { + // Only show the modal when all data is ready + return null; + } + + const removeFromContainer = () => { + const restoreComponent = () => { + addItemToContainerMutation.mutateAsync([usageKey]).then(() => { + showToast(intl.formatMessage(messages.undoRemoveComponentFromContainerToastSuccess)); + }).catch(() => { + showToast(intl.formatMessage(messages.undoRemoveComponentFromContainerToastFailed)); + }); + }; + removeContainerItemMutation.mutateAsync([usageKey]).then(() => { + if (sidebarItemInfo?.id === usageKey) { + // Close sidebar if current component is open + closeLibrarySidebar(); + } + showToast( + intl.formatMessage(messages.removeComponentFromContainerSuccess), + { + label: intl.formatMessage(messages.undoRemoveComponentFromContainerToastAction), + onClick: restoreComponent, + }, + ); + }).catch(() => { + showToast(intl.formatMessage(messages.removeComponentFromContainerFailure)); + }); + + close(); + }; + + const removeText = intl.formatMessage(messages.removeComponentConfirm, { + componentName: {component?.displayName}, + parentContainerName: {container?.displayName}, + }); + + return ( + + ); +}; + +export default ComponentRemover; diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index 5127793fe..f1e3b89d7 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -56,26 +56,21 @@ const messages = defineMessages({ defaultMessage: 'Delete Component', description: 'Title text for the warning displayed before deleting a component', }, - deleteComponentNamePlaceholder: { - id: 'course-authoring.library-authoring.component.delete-confirmation-placeholder', + componentNamePlaceholder: { + id: 'course-authoring.library-authoring.component.name-confirmation-placeholder', defaultMessage: 'this component', description: 'Text shown in place of the component\'s title while we\'re loading the title', }, deleteComponentConfirm: { id: 'course-authoring.library-authoring.component.delete-confirmation-text', - defaultMessage: 'Delete {componentName}? {message}', + defaultMessage: 'Are you sure you want to permanently delete {componentName}? This cannot be undone and will remove it from your library. {message}', description: 'Confirmation text to display before deleting a component', }, deleteComponentConfirmCourse: { id: 'course-authoring.library-authoring.component.delete-confirmation-msg-1', - defaultMessage: 'If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.', + defaultMessage: 'This component is used {courseCount, plural, one {{courseCountText} time} other {{courseCountText} times}} in courses, and will stop receiving updates there.', description: 'First part of confirmation message to display before deleting a component', }, - deleteComponentConfirmCourseSmall: { - id: 'course-authoring.library-authoring.component.delete-confirmation-text.course-small', - defaultMessage: 'Any course instances will stop receiving library updates.', - description: 'Small message text about courses when deleting a component', - }, deleteComponentConfirmUnits: { id: 'course-authoring.library-authoring.component.delete-confirmation-text.units', defaultMessage: 'By deleting this component, you will also be deleting it from {unit} in this library.', @@ -191,6 +186,16 @@ const messages = defineMessages({ defaultMessage: 'Remove from unit', description: 'Text of the menu item to remove a component from a unit', }, + removeComponentWarningTitle: { + id: 'course-authoring.library-authoring.component.remove-confirmation-title', + defaultMessage: 'Remove Component', + description: 'Title text for the warning displayed before removing a component from a container', + }, + removeComponentConfirm: { + id: 'course-authoring.library-authoring.component.remove-confirmation-text', + defaultMessage: 'Are you sure you want to remove {componentName} from {parentContainerName}? Removing this component will not delete it from the library.', + description: 'Confirmation text to display before removing a container from its parent', + }, removeComponentFromContainerSuccess: { id: 'course-authoring.library-authoring.component.remove-from-container-success', defaultMessage: 'Component successfully removed', @@ -216,6 +221,11 @@ const messages = defineMessages({ defaultMessage: 'Failed to undo remove component operation', description: 'Message to display on failure to undo delete component', }, + componentRemoveButtonLabel: { + id: 'course-authoring.library-authoring.component.remove-button-label', + defaultMessage: 'Remove', + description: 'Button to confirm removal of a component from its parent container', + }, containerPreviewText: { id: 'course-authoring.library-authoring.container.preview.text', defaultMessage: 'Contains {children}.', @@ -228,12 +238,12 @@ const messages = defineMessages({ }, removeContainerConfirm: { id: 'course-authoring.library-authoring.container.remove-confirmation-text', - defaultMessage: 'Remove {containerName} from {parentContainerType} {parentContainerName}? Removing this {containerType} will not delete it from the library.', + defaultMessage: 'Are you sure you want to remove {containerName} from {parentContainerName}? This will not delete the {containerType} from your library.', description: 'Confirmation text to display before removing a container from its parent', }, removeContainerButton: { id: 'course-authoring.library-authoring.container.confirm-remove-button', - defaultMessage: 'Remove {containerName}', + defaultMessage: 'Remove', description: 'Button to confirm removal of a container from its parent', }, }); diff --git a/src/library-authoring/containers/ContainerCard.test.tsx b/src/library-authoring/containers/ContainerCard.test.tsx index 020aacfe8..4c8a6aefe 100644 --- a/src/library-authoring/containers/ContainerCard.test.tsx +++ b/src/library-authoring/containers/ContainerCard.test.tsx @@ -1,10 +1,11 @@ import userEvent from '@testing-library/user-event'; import type MockAdapter from 'axios-mock-adapter'; +import { mockContentSearchConfig, mockSearchResult, hydrateSearchResult } from '@src/search-manager/data/api.mock'; import { initializeMocks, render as baseRender, screen, waitFor, fireEvent, -} from '../../testUtils'; +} from '@src/testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks'; import { type ContainerHit, PublishStatus } from '../../search-manager'; @@ -44,6 +45,7 @@ const getContainerHitSample = (containerType: ContainerType = ContainerType.Unit } as ContainerHit); mockContentLibrary.applyMock(); +mockContentSearchConfig.applyMock(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -190,13 +192,16 @@ describe('', () => { it('should delete the container from the menu & restore the container', async () => { const user = userEvent.setup(); axiosMock.onDelete(getLibraryContainerApiUrl(getContainerHitSample().usageKey)).reply(200); + // Mock the search result to get the display name of the container on the Modal + mockSearchResult(hydrateSearchResult([{ + displayName: 'unit Display Name', + }])); render(); // Open menu expect(screen.getByTestId('container-card-menu-toggle')).toBeInTheDocument(); await user.click(screen.getByTestId('container-card-menu-toggle')); - // Click on Delete Item const deleteMenuItem = screen.getByRole('button', { name: 'Delete' }); expect(deleteMenuItem).toBeInTheDocument(); @@ -241,7 +246,8 @@ describe('', () => { fireEvent.click(deleteMenuItem); // Confirm delete Modal is open - expect(screen.getByText('Delete Unit')); + const modal = await screen.findByRole('dialog', { name: 'Delete Unit' }); + expect(modal).toBeVisible(); const deleteButton = screen.getByRole('button', { name: /delete/i }); fireEvent.click(deleteButton); @@ -446,7 +452,8 @@ describe('', () => { fireEvent.click(removeMenuItem); // Confirm remove Modal is open - expect(await screen.findByText(/will not delete it from the library/i)).toBeInTheDocument(); + const regex = new RegExp(`will not delete the ${containerType} from your library`, 'i'); + expect(await screen.findByText(regex)).toBeInTheDocument(); const removeButton = screen.getByRole('button', { name: /remove/i }); fireEvent.click(removeButton); diff --git a/src/library-authoring/containers/ContainerCard.tsx b/src/library-authoring/containers/ContainerCard.tsx index e3e946c3c..028bf4e23 100644 --- a/src/library-authoring/containers/ContainerCard.tsx +++ b/src/library-authoring/containers/ContainerCard.tsx @@ -125,14 +125,12 @@ export const ContainerMenu = ({ containerKey, displayName } : ContainerMenuProps {isConfirmingDelete && ( )} {isConfirmingRemove && ( { const mocks = initializeMocks(); mockShowToast = mocks.mockShowToast; - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [{ - blockType: context, - displayName: `Test ${context}`, - }], - }, - ], - }); - }); - - it(`<${context}> is invisible when isOpen is false`, async () => { - const mockClose = jest.fn(); - const { containerId } = getContainerDetails(context); - render(, renderArgs); - - const modal = screen.queryByRole('dialog', { name: new RegExp(`Delete ${context}`, 'i') }); - expect(modal).not.toBeInTheDocument(); + mockSearchResult(hydrateSearchResult([{ + blockType: context, + displayName: `Test ${context}`, + }])); }); it(`<${context}> should show a confirmation prompt the card with title and description`, async () => { @@ -92,7 +73,6 @@ let mockShowToast: { (message: string, action?: ToastActionData | undefined): vo const { containerId } = getContainerDetails(context); render(, renderArgs); @@ -111,7 +91,7 @@ let mockShowToast: { (message: string, action?: ToastActionData | undefined): vo it(`<${context}> deletes the block when confirmed, shows a toast with undo option and restores block on undo`, async () => { const mockCancel = jest.fn(); const { containerId } = getContainerDetails(context); - render(, renderArgs); + render(, renderArgs); const modal = await screen.findByRole('dialog', { name: new RegExp(`Delete ${context}`, 'i') }); expect(modal).toBeVisible(); @@ -138,23 +118,17 @@ let mockShowToast: { (message: string, action?: ToastActionData | undefined): vo if (!parent) { return; } - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [{ - [`${parent}s`]: { - displayName: [`${parent} 1`], - key: [`${parent}1`], - }, - blockType: context, - }], - }, - ], - }); + mockSearchResult(hydrateSearchResult([{ + [`${parent}s`]: { + displayName: [`${parent} 1`], + key: [`${parent}1`], + }, + blockType: context, + }])); + render( , renderArgs, @@ -173,23 +147,16 @@ let mockShowToast: { (message: string, action?: ToastActionData | undefined): vo if (!parent) { return; } - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [{ - [`${parent}s`]: { - displayName: [`${parent} 1`, `${parent} 2`], - key: [`${parent}1`, `${parent}2`], - }, - blockType: context, - }], - }, - ], - }); + mockSearchResult(hydrateSearchResult([{ + [`${parent}s`]: { + displayName: [`${parent} 1`, `${parent} 2`], + key: [`${parent}1`, `${parent}2`], + }, + blockType: context, + }])); render( , renderArgs, diff --git a/src/library-authoring/containers/ContainerDeleter.tsx b/src/library-authoring/containers/ContainerDeleter.tsx index c3497d5ce..f878ca25e 100644 --- a/src/library-authoring/containers/ContainerDeleter.tsx +++ b/src/library-authoring/containers/ContainerDeleter.tsx @@ -1,27 +1,37 @@ import { useCallback, useContext, useMemo } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon } from '@openedx/paragon'; -import { Error, Warning, School } from '@openedx/paragon/icons'; +import { Error, Delete, School } from '@openedx/paragon/icons'; import DeleteModal from '@src/generic/delete-modal/DeleteModal'; import { ToastContext } from '@src/generic/toast-context'; import { ContainerType } from '@src/generic/key-utils'; -import { ContainerHit } from '@src/search-manager'; +import { type ContainerHit } from '@src/search-manager'; import { useEntityLinks } from '@src/course-libraries/data/apiHooks'; -import { LoadingSpinner } from '@src/generic/Loading'; import { useSidebarContext } from '../common/context/SidebarContext'; +import { useLibraryContext } from '../common/context/LibraryContext'; import { useContentFromSearchIndex, useDeleteContainer, useRestoreContainer } from '../data/apiHooks'; import messages from './messages'; type ContainerDeleterProps = { - isOpen: boolean, close: () => void, containerId: string, }; +type ContainerParents = { + displayName?: string[], + key?: string[], +}; + +const getOtherParentContainers = (containerParents?: ContainerParents, currentParentId?: string) => { + const currentParentIndex = containerParents?.key?.findIndex((id) => id === currentParentId); + return containerParents?.displayName?.filter( + (_, index) => index !== currentParentIndex, + ); +}; + const ContainerDeleter = ({ - isOpen, close, containerId, }: ContainerDeleterProps) => { @@ -30,20 +40,24 @@ const ContainerDeleter = ({ sidebarItemInfo, closeLibrarySidebar, } = useSidebarContext(); + const { + containerId: parentContainerId, + } = useLibraryContext(); const deleteContainerMutation = useDeleteContainer(containerId); const restoreContainerMutation = useRestoreContainer(containerId); const { showToast } = useContext(ToastContext); - const { hits, isLoading } = useContentFromSearchIndex([containerId]); + const { hits, isPending } = useContentFromSearchIndex([containerId]); const containerData = (hits as ContainerHit[])?.[0]; const { data: dataDownstreamLinks, - isLoading: linksIsLoading, + isPending: isPendingLinks, } = useEntityLinks({ upstreamKey: containerId, contentType: 'containers' }); const downstreamCount = dataDownstreamLinks?.length ?? 0; const messageMap = useMemo(() => { const containerType = containerData?.blockType; - let parentCount = 0; + let otherParentContainers: string[] | undefined; + let otherParentCount = 0; let parentMessage: React.ReactNode; switch (containerType) { case ContainerType.Section: @@ -51,42 +65,44 @@ const ContainerDeleter = ({ title: intl.formatMessage(messages.deleteSectionWarningTitle), parentMessage: '', courseCount: downstreamCount, - courseMessage: messages.deleteSectionCourseMessaage, + courseMessage: messages.deleteSectionCourseMessage, deleteSuccess: intl.formatMessage(messages.deleteSectionSuccess), deleteError: intl.formatMessage(messages.deleteSectionFailed), undoDeleteError: messages.undoDeleteSectionToastFailed, }; case ContainerType.Subsection: - parentCount = containerData?.sections?.displayName?.length || 0; - if (parentCount === 1) { + otherParentContainers = getOtherParentContainers(containerData?.sections, parentContainerId); + otherParentCount = otherParentContainers?.length || 0; + if (otherParentCount === 1) { parentMessage = intl.formatMessage( messages.deleteSubsectionParentMessage, { parentName: {containerData?.sections?.displayName?.[0]} }, ); - } else if (parentCount > 1) { + } else if (otherParentCount > 1) { parentMessage = intl.formatMessage(messages.deleteSubsectionMultipleParentMessage, { - parentCount: {parentCount}, + parentCount: {otherParentCount}, }); } return { title: intl.formatMessage(messages.deleteSubsectionWarningTitle), parentMessage, courseCount: downstreamCount, - courseMessage: messages.deleteSubsectionCourseMessaage, + courseMessage: messages.deleteSubsectionCourseMessage, deleteSuccess: intl.formatMessage(messages.deleteSubsectionSuccess), deleteError: intl.formatMessage(messages.deleteSubsectionFailed), undoDeleteError: messages.undoDeleteSubsectionToastFailed, }; - default: - parentCount = containerData?.subsections?.displayName?.length || 0; - if (parentCount === 1) { + default: // Unit + otherParentContainers = getOtherParentContainers(containerData?.subsections, parentContainerId); + otherParentCount = otherParentContainers?.length || 0; + if (otherParentCount === 1) { parentMessage = intl.formatMessage( messages.deleteUnitParentMessage, - { parentName: {containerData?.subsections?.displayName?.[0]} }, + { parentName: {otherParentContainers?.[0]} }, ); - } else if (parentCount > 1) { + } else if (otherParentCount > 1) { parentMessage = intl.formatMessage(messages.deleteUnitMultipleParentMessage, { - parentCount: {parentCount}, + parentCount: {otherParentCount}, }); } return { @@ -101,8 +117,8 @@ const ContainerDeleter = ({ } }, [containerData, downstreamCount, messages, intl]); - const deleteText = intl.formatMessage(messages.deleteUnitConfirm, { - unitName: {containerData?.displayName}, + const deleteText = intl.formatMessage(messages.deleteContainerConfirm, { + containerName: {containerData?.displayName}, message: (
{messageMap.parentMessage && ( @@ -154,14 +170,20 @@ const ContainerDeleter = ({ }); }, [sidebarItemInfo, showToast, deleteContainerMutation, messageMap]); + // istanbul ignore if: loading state + if (isPending || isPendingLinks) { + // Only render the modal after loading + return null; + } + return ( : deleteText} + icon={Delete} + description={deleteText} onDeleteSubmit={onDelete} /> ); diff --git a/src/library-authoring/containers/ContainerInfo.test.tsx b/src/library-authoring/containers/ContainerInfo.test.tsx index db4d9d4c9..b8f378829 100644 --- a/src/library-authoring/containers/ContainerInfo.test.tsx +++ b/src/library-authoring/containers/ContainerInfo.test.tsx @@ -8,7 +8,7 @@ import { } from '@src/testUtils'; import { ContainerType } from '@src/generic/key-utils'; import type { ToastActionData } from '@src/generic/toast-context'; -import { mockContentSearchConfig, mockSearchResult } from '@src/search-manager/data/api.mock'; +import { mockContentSearchConfig, mockSearchResult, hydrateSearchResult } from '@src/search-manager/data/api.mock'; import { mockContentLibrary, mockGetContainerChildren, @@ -162,16 +162,10 @@ let mockShowToast: { (message: string, action?: ToastActionData | undefined): vo describe(` with containerType: ${containerType}`, () => { beforeEach(() => { ({ axiosMock, mockShowToast } = initializeMocks()); - mockSearchResult({ - results: [ // @ts-ignore - { - hits: [{ - blockType: containerType, - displayName: `Test ${containerType}`, - }], - }, - ], - }); + mockSearchResult(hydrateSearchResult([{ + blockType: containerType, + displayName: `Test ${containerType}`, + }])); }); it(`should delete the ${containerType} using the menu`, async () => { @@ -189,7 +183,8 @@ let mockShowToast: { (message: string, action?: ToastActionData | undefined): vo fireEvent.click(deleteMenuItem); // Confirm delete Modal is open - expect(screen.getByText(`Delete ${containerType[0].toUpperCase()}${containerType.slice(1)}`)); + const modal = await screen.findByRole('dialog', { name: `Delete ${containerType[0].toUpperCase()}${containerType.slice(1)}` }); + expect(modal).toBeInTheDocument(); const deleteButton = screen.getByRole('button', { name: /delete/i }); fireEvent.click(deleteButton); diff --git a/src/library-authoring/containers/ContainerInfo.tsx b/src/library-authoring/containers/ContainerInfo.tsx index 23c3d063c..2b36a0764 100644 --- a/src/library-authoring/containers/ContainerInfo.tsx +++ b/src/library-authoring/containers/ContainerInfo.tsx @@ -60,11 +60,12 @@ const ContainerMenu = ({ containerId }: ContainerPreviewProps) => { - + {isConfirmingDelete && ( + + )} ); }; diff --git a/src/library-authoring/containers/ContainerRemover.tsx b/src/library-authoring/containers/ContainerRemover.tsx index 69196cafb..2261edb59 100644 --- a/src/library-authoring/containers/ContainerRemover.tsx +++ b/src/library-authoring/containers/ContainerRemover.tsx @@ -1,24 +1,24 @@ import { useCallback, useContext } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { Warning } from '@openedx/paragon/icons'; import { capitalize } from 'lodash'; import DeleteModal from '@src/generic/delete-modal/DeleteModal'; import { ToastContext } from '@src/generic/toast-context'; import { getBlockType } from '@src/generic/key-utils'; + import { useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryContext } from '../common/context/LibraryContext'; import { useContainer, useRemoveContainerChildren } from '../data/apiHooks'; import messages from '../components/messages'; type ContainerRemoverProps = { - isOpen: boolean, close: () => void, containerKey: string, displayName: string, }; const ContainerRemover = ({ - isOpen, close, containerKey, displayName, @@ -32,7 +32,7 @@ const ContainerRemover = ({ const { showToast } = useContext(ToastContext); const removeContainerMutation = useRemoveContainerChildren(containerId); - const { data: container } = useContainer(containerId); + const { data: container, isPending } = useContainer(containerId); const itemType = getBlockType(containerKey); const removeWarningTitle = intl.formatMessage(messages.removeContainerWarningTitle, { @@ -40,10 +40,9 @@ const ContainerRemover = ({ }); const removeText = intl.formatMessage(messages.removeContainerConfirm, { - containerName: {capitalize(itemType)} {displayName}, + containerName: {displayName}, containerType: capitalize(itemType), - parentContainerType: capitalize(container?.containerType), - parentContainerName: container?.displayName, + parentContainerName: {container?.displayName}, }); const removeSuccess = intl.formatMessage(messages.removeComponentFromContainerSuccess); @@ -72,18 +71,23 @@ const ContainerRemover = ({ close, ]); + // istanbul ignore if: loading state + if (isPending) { + // Only render when data is ready + return null; + } + return ( ); }; diff --git a/src/library-authoring/containers/messages.ts b/src/library-authoring/containers/messages.ts index 44da1dfbf..0cd961a12 100644 --- a/src/library-authoring/containers/messages.ts +++ b/src/library-authoring/containers/messages.ts @@ -101,7 +101,7 @@ const messages = defineMessages({ defaultMessage: 'Delete Section', description: 'Title text for the warning displayed before deleting a Section', }, - deleteSectionCourseMessaage: { + deleteSectionCourseMessage: { id: 'course-authoring.library-authoring.section.delete-parent-message', defaultMessage: 'This section is used {courseCount, plural, one {{courseCountText} time} other {{courseCountText} times}} in courses, and will stop receiving updates there.', description: 'Course usage details shown before deleting a section', @@ -121,7 +121,7 @@ const messages = defineMessages({ defaultMessage: 'Delete Subsection', description: 'Title text for the warning displayed before deleting a Subsection', }, - deleteSubsectionCourseMessaage: { + deleteSubsectionCourseMessage: { id: 'course-authoring.library-authoring.subsection.delete-course-message', defaultMessage: 'This subsection is used {courseCount, plural, one {{courseCountText} time} other {{courseCountText} times}} in courses, and will stop receiving updates there.', description: 'Course usage details shown before deleting a subsection', @@ -146,10 +146,10 @@ const messages = defineMessages({ defaultMessage: 'This unit is used {courseCount, plural, one {{courseCountText} time} other {{courseCountText} times}} in courses, and will stop receiving updates there.', description: 'Course usage details shown before deleting a unit', }, - deleteUnitConfirm: { - id: 'course-authoring.library-authoring.unit.delete-confirmation-text', - defaultMessage: 'Delete {unitName}? {message}', - description: 'Confirmation text to display before deleting a unit', + deleteContainerConfirm: { + id: 'course-authoring.library-authoring.container.delete-confirmation-text', + defaultMessage: 'Are you sure you want to permanently delete {containerName}? This cannot be undone and will remove it from your library. {message}', + description: 'Confirmation text to display before deleting a container', }, deleteUnitSuccess: { id: 'course-authoring.library-authoring.unit.delete.success', diff --git a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx index 5839d6575..e99509f01 100644 --- a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx +++ b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx @@ -25,10 +25,10 @@ export const LibrarySubsectionPage = () => { const { libraryId, containerId } = useLibraryContext(); const { sidebarItemInfo } = useSidebarContext(); - const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); + const { data: libraryData, isPending: isLibPending } = useContentLibrary(libraryId); // fetch subsectionData from index as it includes its parent sections as well. const { - hits, isLoading, isError, error, + hits, isPending, isError, error, } = useContentFromSearchIndex(containerId ? [containerId] : []); const subsectionData = (hits as ContainerHit[])?.[0]; @@ -38,7 +38,7 @@ export const LibrarySubsectionPage = () => { } // Only show loading if section or library data is not fetched from index yet - if (isLibLoading || isLoading) { + if (isLibPending || isPending) { return ; } diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx index 2381cbc5a..07d40212d 100644 --- a/src/library-authoring/units/LibraryUnitBlocks.tsx +++ b/src/library-authoring/units/LibraryUnitBlocks.tsx @@ -290,7 +290,6 @@ export const LibraryUnitBlocks = ({ unitId, readOnly: componentReadOnly }: Libra > {orderedBlocks?.map((block, idx) => ( // A container can have multiple instances of the same block - // eslint-disable-next-line react/no-array-index-key ', () => { }); it('should remove a component & restore from component card', async () => { + const user = userEvent.setup(); const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(200); renderLibraryUnitPage(); expect(await screen.findByText('text block 0')).toBeInTheDocument(); const menu = screen.getAllByRole('button', { name: /component actions menu/i })[0]; - fireEvent.click(menu); + await user.click(menu); const removeButton = await screen.findByText('Remove from unit'); - fireEvent.click(removeButton); + await user.click(removeButton); + + const modal = await screen.findByRole('dialog', { name: 'Remove Component' }); + expect(modal).toBeVisible(); + + const confirmButton = await within(modal).findByRole('button', { name: 'Remove' }); + await user.click(confirmButton); await waitFor(() => { expect(axiosMock.history.delete[0].url).toEqual(url); @@ -385,16 +392,23 @@ describe('', () => { }); it('should show error on remove a component', async () => { + const user = userEvent.setup(); const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(404); renderLibraryUnitPage(); expect(await screen.findByText('text block 0')).toBeInTheDocument(); const menu = screen.getAllByRole('button', { name: /component actions menu/i })[0]; - fireEvent.click(menu); + await user.click(menu); const removeButton = await screen.findByText('Remove from unit'); - fireEvent.click(removeButton); + await user.click(removeButton); + + const modal = await screen.findByRole('dialog', { name: 'Remove Component' }); + expect(modal).toBeVisible(); + + const confirmButton = await within(modal).findByRole('button', { name: 'Remove' }); + await user.click(confirmButton); await waitFor(() => { expect(axiosMock.history.delete[0].url).toEqual(url); @@ -403,16 +417,23 @@ describe('', () => { }); it('should show error on restore removed component', async () => { + const user = userEvent.setup(); const url = getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.unitId); axiosMock.onDelete(url).reply(200); renderLibraryUnitPage(); expect(await screen.findByText('text block 0')).toBeInTheDocument(); const menu = screen.getAllByRole('button', { name: /component actions menu/i })[0]; - fireEvent.click(menu); + await user.click(menu); const removeButton = await screen.findByText('Remove from unit'); - fireEvent.click(removeButton); + await user.click(removeButton); + + const modal = await screen.findByRole('dialog', { name: 'Remove Component' }); + expect(modal).toBeVisible(); + + const confirmButton = await within(modal).findByRole('button', { name: 'Remove' }); + await user.click(confirmButton); await waitFor(() => { expect(axiosMock.history.delete[0].url).toEqual(url); @@ -446,10 +467,16 @@ describe('', () => { const { findByRole, findByText } = within(sidebar); const menu = await findByRole('button', { name: /component actions menu/i }); - fireEvent.click(menu); + await user.click(menu); const removeButton = await findByText('Remove from unit'); - fireEvent.click(removeButton); + await user.click(removeButton); + + const modal = await screen.findByRole('dialog', { name: 'Remove Component' }); + expect(modal).toBeVisible(); + + const confirmButton = await within(modal).findByRole('button', { name: 'Remove' }); + await user.click(confirmButton); await waitFor(() => { expect(axiosMock.history.delete[0].url).toEqual(url); diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx index 170c6cfc3..f8fd6ea16 100644 --- a/src/library-authoring/units/LibraryUnitPage.tsx +++ b/src/library-authoring/units/LibraryUnitPage.tsx @@ -35,10 +35,10 @@ export const LibraryUnitPage = () => { const { sidebarItemInfo } = useSidebarContext(); - const { data: libraryData, isLoading: isLibLoading } = useContentLibrary(libraryId); + const { data: libraryData, isPending: isLibPending } = useContentLibrary(libraryId); // fetch unitData from index as it includes its parent subsections as well. const { - hits, isLoading, isError, error, + hits, isPending, isError, error, } = useContentFromSearchIndex(containerId ? [containerId] : []); const unitData = (hits as ContainerHit[])?.[0]; @@ -48,7 +48,7 @@ export const LibraryUnitPage = () => { } // Only show loading if unit or library data is not fetched from index yet - if (isLibLoading || isLoading) { + if (isLibPending || isPending) { return ; } diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index d53ceb675..87f70a983 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -40,7 +40,7 @@ export interface SearchContextData { defaultSearchSortOrder: SearchSortOption; hits: HitType[]; totalHits: number; - isLoading: boolean; + isPending: boolean; hasNextPage: boolean | undefined; isFetchingNextPage: boolean; fetchNextPage: () => void; diff --git a/src/search-manager/data/api.mock.ts b/src/search-manager/data/api.mock.ts index d11988a5e..59ff21f55 100644 --- a/src/search-manager/data/api.mock.ts +++ b/src/search-manager/data/api.mock.ts @@ -23,6 +23,9 @@ mockContentSearchConfig.applyMock = () => ( /** * Mock all future Meilisearch searches with the given response. * + * If you want to pass only the hits, use `hydrateSearchResult()` to create a full + * MultiSearchResponse object. + * * For a given test suite, this mock will stay in effect until you call it with * a different mock response, or you call `fetchMock.mockReset()` */ @@ -45,6 +48,25 @@ export function mockSearchResult( }, { overwriteRoutes: true }); } +/** Helper to create a full MultiSearchResponse object from an array of hits. + * You can then pass the result to `mockSearchResult()`. + */ +export const hydrateSearchResult: (hits: any[]) => MultiSearchResponse = (hits) => ({ + results: [ + { + hits, + offset: 0, + limit: 20, + nbHits: 0, + exhaustiveNbHits: true, + processingTimeMs: 1, + query: '', + params: '', + indexUid: 'studio', + }, + ], +}); + /** * Mock the block types returned by the API. */ diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index 821aece67..51881a1c2 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -177,7 +177,7 @@ export const useContentSearchResults = ({ problemTypes: pages?.[0]?.problemTypes ?? {}, publishStatus: pages?.[0]?.publishStatus ?? {}, status: query.status, - isLoading: query.isPending, + isPending: query.isPending, isError: query.isError, error: query.error, isFetchingNextPage: query.isFetchingNextPage,