loads data
mockLibraryBlockMetadata.applyMock();
+mockContentSearchConfig.applyMock();
const mockDelete = mockDeleteLibraryBlock.applyMock();
const mockRestore = mockRestoreLibraryBlock.applyMock();
@@ -29,6 +31,13 @@ describe('', () => {
beforeEach(() => {
const mocks = initializeMocks();
mockShowToast = mocks.mockShowToast;
+ mockSearchResult({
+ results: [ // @ts-ignore
+ {
+ hits: [],
+ },
+ ],
+ });
});
it('is invisible when isConfirmingDelete is false', async () => {
@@ -77,4 +86,68 @@ describe('', () => {
expect(mockShowToast).toHaveBeenCalledWith('Undo successful');
});
});
+
+ 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'],
+ },
+ }],
+ },
+ ],
+ });
+ render(
+ ,
+ renderArgs,
+ );
+
+ const modal = await screen.findByRole('dialog', { name: 'Delete Component' });
+ expect(modal).toBeVisible();
+
+ expect(await screen.findByText(
+ /by deleting this component, you will also be deleting it from in this library\./i,
+ )).toBeInTheDocument();
+ expect(screen.getByText(/unit 1/i)).toBeInTheDocument();
+ });
+
+ 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'],
+ },
+ }],
+ },
+ ],
+ });
+ render(
+ ,
+ renderArgs,
+ );
+
+ const modal = await screen.findByRole('dialog', { name: 'Delete Component' });
+ expect(modal).toBeVisible();
+
+ expect(await screen.findByText(
+ /by deleting this component, you will also be deleting it from in this library\./i,
+ )).toBeInTheDocument();
+ expect(screen.getByText(/2 units/i)).toBeInTheDocument();
+ });
});
diff --git a/src/library-authoring/components/ComponentDeleter.tsx b/src/library-authoring/components/ComponentDeleter.tsx
index c48dedec1..629795ad0 100644
--- a/src/library-authoring/components/ComponentDeleter.tsx
+++ b/src/library-authoring/components/ComponentDeleter.tsx
@@ -1,13 +1,19 @@
import React, { useCallback, useContext } from 'react';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
-import { CalendarViewDay, School, Warning } from '@openedx/paragon/icons';
+import { School, Warning, Info } from '@openedx/paragon/icons';
import { useSidebarContext } from '../common/context/SidebarContext';
-import { useDeleteLibraryBlock, useLibraryBlockMetadata, useRestoreLibraryBlock } from '../data/apiHooks';
+import {
+ useComponentsFromSearchIndex,
+ 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.
@@ -63,22 +69,44 @@ const ComponentDeleter = ({ usageKey, ...props }: Props) => {
}
}, [usageKey, sidebarComponentUsageKey, closeLibrarySidebar]);
+ const { hits } = useComponentsFromSearchIndex([usageKey]);
+ const componentHit = (hits as ContentHit[])?.[0];
+
if (!props.isConfirmingDelete) {
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 deleteText = intl.formatMessage(messages.deleteComponentConfirm, {
componentName: ,
message: (
<>
- {intl.formatMessage(messages.deleteComponentConfirmMsg1)}
-
-
-
- {intl.formatMessage(messages.deleteComponentConfirmMsg2)}
+ {unitsMessage
+ ? intl.formatMessage(messages.deleteComponentConfirmCourseSmall)
+ : intl.formatMessage(messages.deleteComponentConfirmCourse)}
+ {unitsMessage && (
+
+
+
+ {unitsMessage},
+ }}
+ />
+
+
+ )}
>
),
});
diff --git a/src/library-authoring/components/ComponentMenu.tsx b/src/library-authoring/components/ComponentMenu.tsx
index ba3711711..b5bd16749 100644
--- a/src/library-authoring/components/ComponentMenu.tsx
+++ b/src/library-authoring/components/ComponentMenu.tsx
@@ -148,7 +148,13 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
-
+ {isConfirmingDelete && (
+
+ )}
);
};
diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts
index 8248b4f43..e107eedd2 100644
--- a/src/library-authoring/components/messages.ts
+++ b/src/library-authoring/components/messages.ts
@@ -71,15 +71,20 @@ const messages = defineMessages({
defaultMessage: 'Delete {componentName}? {message}',
description: 'Confirmation text to display before deleting a component',
},
- deleteComponentConfirmMsg1: {
+ 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.',
description: 'First part of confirmation message to display before deleting a component',
},
- deleteComponentConfirmMsg2: {
- id: 'course-authoring.library-authoring.component.delete-confirmation-msg-2',
- defaultMessage: 'If this component has been used in any units, it will also be deleted from those units.',
- description: 'Second 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.',
+ description: 'Message text about units when deleting a component',
},
deleteComponentCancelButton: {
id: 'course-authoring.library-authoring.component.cancel-delete-button',
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 65b9445ad..49b5e72c3 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -12,6 +12,7 @@ import { useCallback } from 'react';
import { getLibraryId } from '../../generic/key-utils';
import * as api from './api';
import { VersionSpec } from '../LibraryBlock';
+import { useContentSearchConnection, useContentSearchResults } from '../../search-manager';
export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => {
// Invalidate all content queries related to this library.
@@ -802,3 +803,19 @@ export const usePublishContainer = (containerId: string) => {
},
});
};
+
+/**
+ * Use this mutations to get a list of components from the search index
+ */
+export const useComponentsFromSearchIndex = (componentIds: string[]) => {
+ const { client, indexName } = useContentSearchConnection();
+ return useContentSearchResults({
+ client,
+ indexName,
+ searchKeywords: '',
+ extraFilter: [`usage_key IN ["${componentIds.join('","')}"]`],
+ limit: componentIds.length,
+ enabled: !!componentIds.length,
+ skipBlockTypeFetch: true,
+ });
+};
diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts
index e1a6aeaa3..01811883c 100644
--- a/src/search-manager/data/api.ts
+++ b/src/search-manager/data/api.ts
@@ -142,6 +142,7 @@ export interface ContentHit extends BaseContentHit {
content?: ContentDetails;
lastPublished: number | null;
collections: { displayName?: string[], key?: string[] };
+ units: { displayName?: string[], key?: string[] };
published?: ContentPublishedData;
publishStatus: PublishStatus;
formatted: BaseContentHit['formatted'] & { published?: ContentPublishedData, };