perf: use Library search results to populate container card preview [FC-0083] [TEAK] (#1889)
* fix: several library unit page UX bugs (#1868) * fix: rename "Organize" tab to "Manage" * fix: duplicate key warnings * fix: uniform messages while adding to collection * fix: do not allow units be added to a unit (cherry picked from commit0fdc460c5b) * perf: use Library search results to populate container card preview (#1820) * fix: use Library search results to populate container card preview * feat: show published children when showing only published Unit content * fix: nits (cherry picked from commit24e469542d) --------- Co-authored-by: Rômulo Penido <romulo.penido@gmail.com>
This commit is contained in:
@@ -433,7 +433,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
const { getByRole, queryByText } = within(sidebar);
|
||||
|
||||
await waitFor(() => expect(queryByText(displayName)).toBeInTheDocument());
|
||||
expect(getByRole('tab', { selected: true })).toHaveTextContent('Organize');
|
||||
expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage');
|
||||
const closeButton = getByRole('button', { name: /close/i });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ describe('<AddContent />', () => {
|
||||
await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
|
||||
await waitFor(() => expect(axiosMock.history.patch.length).toEqual(1));
|
||||
await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(collectionComponentUrl));
|
||||
expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this collection.');
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to add content to collection.');
|
||||
});
|
||||
|
||||
it('should stop user from pasting unsupported blocks and show toast', async () => {
|
||||
|
||||
@@ -29,6 +29,8 @@ import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { PickLibraryContentModal } from './PickLibraryContentModal';
|
||||
import { blockTypes } from '../../editors/data/constants/app';
|
||||
|
||||
import { ContentType as LibraryContentTypes } from '../routes';
|
||||
import genericMessages from '../generic/messages';
|
||||
import messages from './messages';
|
||||
import type { BlockTypeMetadata } from '../data/api';
|
||||
import { getContainerTypeFromId, ContainerType } from '../../generic/key-utils';
|
||||
@@ -114,6 +116,9 @@ const AddContentView = ({
|
||||
blockType: 'libraryContent',
|
||||
};
|
||||
|
||||
const extraFilter = unitId ? ['NOT block_type = "unit"', 'NOT type = "collections"'] : undefined;
|
||||
const visibleTabs = unitId ? [LibraryContentTypes.components] : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{(collectionId || unitId) && componentPicker && (
|
||||
@@ -123,6 +128,8 @@ const AddContentView = ({
|
||||
<PickLibraryContentModal
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
extraFilter={extraFilter}
|
||||
visibleTabs={visibleTabs}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -301,7 +308,7 @@ const AddContent = () => {
|
||||
const linkComponent = (opaqueKey: string) => {
|
||||
if (collectionId) {
|
||||
addComponentsToCollectionMutation.mutateAsync([opaqueKey]).catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage));
|
||||
showToast(intl.formatMessage(genericMessages.manageCollectionsFailed));
|
||||
});
|
||||
}
|
||||
if (unitId) {
|
||||
|
||||
@@ -92,7 +92,10 @@ describe('<PickLibraryContentModal />', () => {
|
||||
}
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Content linked successfully.');
|
||||
const text = context === 'collection'
|
||||
? 'Content added to collection.'
|
||||
: 'Content linked successfully.';
|
||||
expect(mockShowToast).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it(`show error when api call fails (${context})`, async () => {
|
||||
@@ -130,8 +133,10 @@ describe('<PickLibraryContentModal />', () => {
|
||||
}
|
||||
});
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
const name = context === 'collection' ? 'collection' : 'container';
|
||||
expect(mockShowToast).toHaveBeenCalledWith(`There was an error linking the content to this ${name}.`);
|
||||
const text = context === 'collection'
|
||||
? 'Failed to add content to collection.'
|
||||
: 'There was an error linking the content to this container.';
|
||||
expect(mockShowToast).toHaveBeenCalledWith(text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ToastContext } from '../../generic/toast-context';
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import type { SelectedComponent } from '../common/context/ComponentPickerContext';
|
||||
import { useAddItemsToCollection, useAddComponentsToContainer } from '../data/apiHooks';
|
||||
import genericMessages from '../generic/messages';
|
||||
import type { ContentType } from '../routes';
|
||||
import messages from './messages';
|
||||
|
||||
interface PickLibraryContentModalFooterProps {
|
||||
@@ -32,12 +34,14 @@ interface PickLibraryContentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
extraFilter?: string[];
|
||||
visibleTabs?: ContentType[],
|
||||
}
|
||||
|
||||
export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
extraFilter,
|
||||
visibleTabs,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -69,16 +73,16 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
if (collectionId) {
|
||||
updateCollectionItemsMutation.mutateAsync(usageKeys)
|
||||
.then(() => {
|
||||
showToast(intl.formatMessage(messages.successAssociateComponentMessage));
|
||||
showToast(intl.formatMessage(genericMessages.manageCollectionsSuccess));
|
||||
})
|
||||
.catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage));
|
||||
showToast(intl.formatMessage(genericMessages.manageCollectionsFailed));
|
||||
});
|
||||
}
|
||||
if (unitId) {
|
||||
updateUnitComponentsMutation.mutateAsync(usageKeys)
|
||||
.then(() => {
|
||||
showToast(intl.formatMessage(messages.successAssociateComponentMessage));
|
||||
showToast(intl.formatMessage(messages.successAssociateComponentToContainerMessage));
|
||||
})
|
||||
.catch(() => {
|
||||
showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage));
|
||||
@@ -109,6 +113,7 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
|
||||
componentPickerMode="multiple"
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
extraFilter={extraFilter}
|
||||
visibleTabs={visibleTabs}
|
||||
/>
|
||||
</StandardModal>
|
||||
);
|
||||
|
||||
@@ -84,15 +84,10 @@ const messages = defineMessages({
|
||||
+ ' The {detail} text provides more information about the error.'
|
||||
),
|
||||
},
|
||||
successAssociateComponentMessage: {
|
||||
id: 'course-authoring.library-authoring.associate-collection-content.success.text',
|
||||
successAssociateComponentToContainerMessage: {
|
||||
id: 'course-authoring.library-authoring.associate-container-content.success.text',
|
||||
defaultMessage: 'Content linked successfully.',
|
||||
description: 'Message when linking of content to a collection in library is success',
|
||||
},
|
||||
errorAssociateComponentToCollectionMessage: {
|
||||
id: 'course-authoring.library-authoring.associate-collection-content.error.text',
|
||||
defaultMessage: 'There was an error linking the content to this collection.',
|
||||
description: 'Message when linking of content to a collection in library fails',
|
||||
description: 'Message when linking of content to a container in library is success',
|
||||
},
|
||||
errorAssociateComponentToContainerMessage: {
|
||||
id: 'course-authoring.library-authoring.associate-container-content.error.text',
|
||||
|
||||
@@ -36,7 +36,7 @@ export const isComponentInfoTab = (tab: string): tab is ComponentInfoTab => (
|
||||
|
||||
export const UNIT_INFO_TABS = {
|
||||
Preview: 'preview',
|
||||
Organize: 'organize',
|
||||
Manage: 'manage',
|
||||
Usage: 'usage',
|
||||
Settings: 'settings',
|
||||
} as const;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
fireEvent,
|
||||
} from '../../testUtils';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import { mockContentLibrary, mockGetContainerChildren } from '../data/api.mocks';
|
||||
import { mockContentLibrary } from '../data/api.mocks';
|
||||
import { type ContainerHit, PublishStatus } from '../../search-manager';
|
||||
import ContainerCard from './ContainerCard';
|
||||
import { getLibraryContainerApiUrl, getLibraryContainerRestoreApiUrl } from '../data/api';
|
||||
@@ -40,7 +40,6 @@ let axiosMock: MockAdapter;
|
||||
let mockShowToast;
|
||||
|
||||
mockContentLibrary.applyMock();
|
||||
mockGetContainerChildren.applyMock();
|
||||
|
||||
const render = (ui: React.ReactElement, showOnlyPublished: boolean = false) => baseRender(ui, {
|
||||
extraWrapper: ({ children }) => (
|
||||
@@ -155,29 +154,54 @@ describe('<ContainerCard />', () => {
|
||||
it('should render no child blocks in card preview', async () => {
|
||||
render(<ContainerCard hit={containerHitSample} />);
|
||||
|
||||
expect(screen.queryByTitle('text block')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTitle('lb:org1:Demo_course:html:text-0')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('+0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render <=5 child blocks in card preview', async () => {
|
||||
const containerWith5Children = {
|
||||
...containerHitSample,
|
||||
usageKey: mockGetContainerChildren.fiveChildren,
|
||||
};
|
||||
content: {
|
||||
childUsageKeys: Array(5).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
|
||||
},
|
||||
} satisfies ContainerHit;
|
||||
render(<ContainerCard hit={containerWith5Children} />);
|
||||
|
||||
expect((await screen.findAllByTitle(/text block */)).length).toBe(5);
|
||||
expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(5);
|
||||
expect(screen.queryByText('+0')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render >5 child blocks with +N in card preview', async () => {
|
||||
const containerWith6Children = {
|
||||
...containerHitSample,
|
||||
usageKey: mockGetContainerChildren.sixChildren,
|
||||
};
|
||||
content: {
|
||||
childUsageKeys: Array(6).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
|
||||
},
|
||||
} satisfies ContainerHit;
|
||||
render(<ContainerCard hit={containerWith6Children} />);
|
||||
|
||||
expect((await screen.findAllByTitle(/text block */)).length).toBe(4);
|
||||
expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(4);
|
||||
expect(screen.queryByText('+2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render published child blocks when rendering a published card preview', async () => {
|
||||
const containerWithPublishedChildren = {
|
||||
...containerHitSample,
|
||||
content: {
|
||||
childUsageKeys: Array(6).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
|
||||
},
|
||||
published: {
|
||||
content: {
|
||||
childUsageKeys: Array(2).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`),
|
||||
},
|
||||
},
|
||||
} satisfies ContainerHit;
|
||||
render(
|
||||
<ContainerCard hit={containerWithPublishedChildren} />,
|
||||
true,
|
||||
);
|
||||
|
||||
expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(2);
|
||||
expect(screen.queryByText('+2')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,12 +12,13 @@ import { MoreVert } from '@openedx/paragon/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
|
||||
import { getBlockType } from '../../generic/key-utils';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import { type ContainerHit, PublishStatus } from '../../search-manager';
|
||||
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
|
||||
import { useLibraryContext } from '../common/context/LibraryContext';
|
||||
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { useContainerChildren, useRemoveItemsFromCollection } from '../data/apiHooks';
|
||||
import { useRemoveItemsFromCollection } from '../data/apiHooks';
|
||||
import { useLibraryRoutes } from '../routes';
|
||||
import AddComponentWidget from './AddComponentWidget';
|
||||
import BaseCard from './BaseCard';
|
||||
@@ -107,21 +108,17 @@ const ContainerMenu = ({ hit } : ContainerMenuProps) => {
|
||||
};
|
||||
|
||||
type ContainerCardPreviewProps = {
|
||||
containerId: string;
|
||||
childUsageKeys: Array<string>;
|
||||
showMaxChildren?: number;
|
||||
};
|
||||
|
||||
const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCardPreviewProps) => {
|
||||
const { data, isLoading, isError } = useContainerChildren(containerId);
|
||||
if (isLoading || isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hiddenChildren = data.length - showMaxChildren;
|
||||
const ContainerCardPreview = ({ childUsageKeys, showMaxChildren = 5 }: ContainerCardPreviewProps) => {
|
||||
const hiddenChildren = childUsageKeys.length - showMaxChildren;
|
||||
return (
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
{
|
||||
data.slice(0, showMaxChildren).map(({ id, blockType, displayName }, idx) => {
|
||||
childUsageKeys.slice(0, showMaxChildren).map((usageKey, idx) => {
|
||||
const blockType = getBlockType(usageKey);
|
||||
let blockPreview: ReactNode;
|
||||
let classNames;
|
||||
|
||||
@@ -133,7 +130,7 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar
|
||||
<Icon
|
||||
src={getItemIcon(blockType)}
|
||||
screenReaderText={blockType}
|
||||
title={displayName}
|
||||
title={usageKey}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -147,7 +144,9 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`container-card-preview-block-${id}`}
|
||||
// A container can have multiple instances of the same block
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${usageKey}-${idx}`}
|
||||
className={classNames}
|
||||
>
|
||||
{blockPreview}
|
||||
@@ -176,6 +175,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
published,
|
||||
publishStatus,
|
||||
usageKey: unitId,
|
||||
content,
|
||||
} = hit;
|
||||
|
||||
const numChildrenCount = showOnlyPublished ? (
|
||||
@@ -186,6 +186,10 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
showOnlyPublished ? formatted.published?.displayName : formatted.displayName
|
||||
) ?? '';
|
||||
|
||||
const childUsageKeys: Array<string> = (
|
||||
showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys
|
||||
) ?? [];
|
||||
|
||||
const { navigateTo } = useLibraryRoutes();
|
||||
|
||||
const openContainer = useCallback(() => {
|
||||
@@ -200,7 +204,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
|
||||
<BaseCard
|
||||
itemType={itemType}
|
||||
displayName={displayName}
|
||||
preview={<ContainerCardPreview containerId={unitId} />}
|
||||
preview={<ContainerCardPreview childUsageKeys={childUsageKeys} />}
|
||||
tags={tags}
|
||||
numChildren={numChildrenCount}
|
||||
actions={(
|
||||
|
||||
@@ -85,7 +85,7 @@ const ContainerOrganize = () => {
|
||||
>
|
||||
<Stack gap={1} direction="horizontal">
|
||||
<Icon src={Tag} />
|
||||
{intl.formatMessage(messages.organizeTabTagsTitle, { count: tagsCount })}
|
||||
{intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })}
|
||||
</Stack>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ExpandMore} />
|
||||
@@ -113,7 +113,7 @@ const ContainerOrganize = () => {
|
||||
>
|
||||
<Stack gap={1} direction="horizontal">
|
||||
<Icon src={BookOpen} />
|
||||
{intl.formatMessage(messages.organizeTabCollectionsTitle, { count: collectionsCount })}
|
||||
{intl.formatMessage(messages.manageTabCollectionsTitle, { count: collectionsCount })}
|
||||
</Stack>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ExpandMore} />
|
||||
|
||||
@@ -120,7 +120,7 @@ const UnitInfo = () => {
|
||||
useEffect(() => {
|
||||
// Show Organize tab if JumpToAddCollections action is set in sidebarComponentInfo
|
||||
if (jumpToCollections) {
|
||||
setSidebarTab(UNIT_INFO_TABS.Organize);
|
||||
setSidebarTab(UNIT_INFO_TABS.Manage);
|
||||
}
|
||||
}, [jumpToCollections, setSidebarTab]);
|
||||
|
||||
@@ -166,7 +166,7 @@ const UnitInfo = () => {
|
||||
onSelect={setSidebarTab}
|
||||
>
|
||||
{renderTab(UNIT_INFO_TABS.Preview, <LibraryUnitBlocks preview />, intl.formatMessage(messages.previewTabTitle))}
|
||||
{renderTab(UNIT_INFO_TABS.Organize, <ContainerOrganize />, intl.formatMessage(messages.organizeTabTitle))}
|
||||
{renderTab(UNIT_INFO_TABS.Manage, <ContainerOrganize />, intl.formatMessage(messages.manageTabTitle))}
|
||||
{renderTab(UNIT_INFO_TABS.Settings, 'Unit Settings', intl.formatMessage(messages.settingsTabTitle))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
|
||||
@@ -11,20 +11,20 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Preview',
|
||||
description: 'Title for preview tab',
|
||||
},
|
||||
organizeTabTitle: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.organize-tab.title',
|
||||
defaultMessage: 'Organize',
|
||||
description: 'Title for organize tab',
|
||||
manageTabTitle: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.manage-tab.title',
|
||||
defaultMessage: 'Manage',
|
||||
description: 'Title for manage tab',
|
||||
},
|
||||
organizeTabTagsTitle: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.organize-tab.tags.title',
|
||||
manageTabTagsTitle: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.manage-tab.tags.title',
|
||||
defaultMessage: 'Tags ({count})',
|
||||
description: 'Title for tags section in organize tab',
|
||||
description: 'Title for tags section in manage tab',
|
||||
},
|
||||
organizeTabCollectionsTitle: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.organize-tab.collections.title',
|
||||
manageTabCollectionsTitle: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.manage-tab.collections.title',
|
||||
defaultMessage: 'Collections ({count})',
|
||||
description: 'Title for collections section in organize tab',
|
||||
description: 'Title for collections section in manage tab',
|
||||
},
|
||||
publishContainerButton: {
|
||||
id: 'course-authoring.library-authoring.container-sidebar.publish-button',
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('<ManageCollections />', () => {
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.patch.length).toEqual(1);
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Item collections updated');
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Content added to collection.');
|
||||
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
||||
collection_keys: ['my-first-collection', 'my-second-collection'],
|
||||
});
|
||||
@@ -103,7 +103,7 @@ describe('<ManageCollections />', () => {
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.patch.length).toEqual(1);
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Item collections updated');
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Content added to collection.');
|
||||
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
||||
collection_keys: ['my-first-collection', 'my-second-collection'],
|
||||
});
|
||||
@@ -133,7 +133,7 @@ describe('<ManageCollections />', () => {
|
||||
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
|
||||
collection_keys: ['my-second-collection'],
|
||||
});
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to update item collections');
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Failed to add content to collection.');
|
||||
expect(screen.queryByRole('search')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ToastContext } from '../../../generic/toast-context';
|
||||
import { CollectionMetadata } from '../../data/api';
|
||||
import { useLibraryContext } from '../../common/context/LibraryContext';
|
||||
import { SidebarActions, useSidebarContext } from '../../common/context/SidebarContext';
|
||||
import genericMessages from '../messages';
|
||||
import messages from './messages';
|
||||
|
||||
interface ManageCollectionsProps {
|
||||
@@ -50,9 +51,9 @@ const CollectionsSelectableBox = ({
|
||||
const handleConfirmation = () => {
|
||||
setBtnState('pending');
|
||||
updateCollectionsMutation.mutateAsync(selectedCollections).then(() => {
|
||||
showToast(intl.formatMessage(messages.manageCollectionsToComponentSuccess));
|
||||
showToast(intl.formatMessage(genericMessages.manageCollectionsSuccess));
|
||||
}).catch(() => {
|
||||
showToast(intl.formatMessage(messages.manageCollectionsToComponentFailed));
|
||||
showToast(intl.formatMessage(genericMessages.manageCollectionsFailed));
|
||||
}).finally(() => {
|
||||
setBtnState('default');
|
||||
onClose();
|
||||
|
||||
@@ -21,16 +21,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Collection selection',
|
||||
description: 'Aria label text for collection selection box',
|
||||
},
|
||||
manageCollectionsToComponentSuccess: {
|
||||
id: 'course-authoring.library-authoring.manage-collections.add-success',
|
||||
defaultMessage: 'Item collections updated',
|
||||
description: 'Message to display on updating item collections',
|
||||
},
|
||||
manageCollectionsToComponentFailed: {
|
||||
id: 'course-authoring.library-authoring.manage-collections.add-failed',
|
||||
defaultMessage: 'Failed to update item collections',
|
||||
description: 'Message to display on failure of updating item collections',
|
||||
},
|
||||
manageCollectionsToComponentConfirmBtn: {
|
||||
id: 'course-authoring.library-authoring.manage-collections.add-confirm-btn',
|
||||
defaultMessage: 'Confirm',
|
||||
|
||||
16
src/library-authoring/generic/messages.ts
Normal file
16
src/library-authoring/generic/messages.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
manageCollectionsSuccess: {
|
||||
id: 'course-authoring.library-authoring.manage-collections.success',
|
||||
defaultMessage: 'Content added to collection.',
|
||||
description: 'Message to display on updating item collections',
|
||||
},
|
||||
manageCollectionsFailed: {
|
||||
id: 'course-authoring.library-authoring.manage-collections.failed',
|
||||
defaultMessage: 'Failed to add content to collection.',
|
||||
description: 'Message to display on failure of updating item collections',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
useUpdateXBlockFields,
|
||||
} from '../data/apiHooks';
|
||||
import { LibraryBlock } from '../LibraryBlock';
|
||||
import { useLibraryRoutes } from '../routes';
|
||||
import { useLibraryRoutes, ContentType } from '../routes';
|
||||
import messages from './messages';
|
||||
import { useSidebarContext } from '../common/context/SidebarContext';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
@@ -200,8 +200,10 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderedBlocks = orderedBlocks?.map((block) => (
|
||||
<IframeProvider key={block.id + block.modified}>
|
||||
const renderedBlocks = orderedBlocks?.map((block, idx) => (
|
||||
// A container can have multiple instances of the same block
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<IframeProvider key={`${block.id}-${idx}-${block.modified}`}>
|
||||
<SortableItem
|
||||
id={block.id}
|
||||
componentStyle={null}
|
||||
@@ -218,16 +220,16 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
|
||||
disabled={preview}
|
||||
>
|
||||
{hidePreviewFor !== block.id && (
|
||||
<div className={classNames('p-3', {
|
||||
'container-mw-md': block.blockType === blockTypes.video,
|
||||
})}
|
||||
>
|
||||
<LibraryBlock
|
||||
usageKey={block.id}
|
||||
version={showOnlyPublished ? 'published' : undefined}
|
||||
minHeight={calculateMinHeight(block)}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames('p-3', {
|
||||
'container-mw-md': block.blockType === blockTypes.video,
|
||||
})}
|
||||
>
|
||||
<LibraryBlock
|
||||
usageKey={block.id}
|
||||
version={showOnlyPublished ? 'published' : undefined}
|
||||
minHeight={calculateMinHeight(block)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SortableItem>
|
||||
</IframeProvider>
|
||||
@@ -245,7 +247,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
|
||||
>
|
||||
{renderedBlocks}
|
||||
</DraggableList>
|
||||
{ !preview && (
|
||||
{!preview && (
|
||||
<div className="d-flex">
|
||||
<div className="w-100 mr-2">
|
||||
<Button
|
||||
@@ -273,7 +275,8 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
|
||||
<PickLibraryContentModal
|
||||
isOpen={isAddLibraryContentModalOpen}
|
||||
onClose={closeAddLibraryContentModal}
|
||||
extraFilter={['NOT block_type = "unit"']}
|
||||
extraFilter={['NOT block_type = "unit"', 'NOT type = "collection"']}
|
||||
visibleTabs={[ContentType.components]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,7 @@ export const LibraryUnitPage = () => {
|
||||
setDefaultTab({
|
||||
collection: COLLECTION_INFO_TABS.Details,
|
||||
component: COMPONENT_INFO_TABS.Manage,
|
||||
unit: UNIT_INFO_TABS.Organize,
|
||||
unit: UNIT_INFO_TABS.Manage,
|
||||
});
|
||||
setHiddenTabs([COMPONENT_INFO_TABS.Preview, UNIT_INFO_TABS.Preview]);
|
||||
return () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ export const getContentSearchConfig = async (): Promise<{ url: string, indexName
|
||||
export interface ContentDetails {
|
||||
htmlContent?: string;
|
||||
capaContent?: string;
|
||||
childUsageKeys?: Array<string>;
|
||||
[k: string]: any;
|
||||
}
|
||||
|
||||
@@ -151,9 +152,10 @@ export interface ContentHit extends BaseContentHit {
|
||||
* Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py
|
||||
*/
|
||||
export interface ContentPublishedData {
|
||||
description?: string,
|
||||
displayName?: string,
|
||||
numChildren?: number,
|
||||
description?: string;
|
||||
displayName?: string;
|
||||
numChildren?: number;
|
||||
content?: ContentDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,6 +173,9 @@ export interface CollectionHit extends BaseContentHit {
|
||||
* Information about a single container returned in the search results
|
||||
* Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py
|
||||
*/
|
||||
interface ContainerHitContent {
|
||||
childUsageKeys?: string[],
|
||||
}
|
||||
export interface ContainerHit extends BaseContentHit {
|
||||
type: 'library_container';
|
||||
blockType: 'unit'; // This should be expanded to include other container types
|
||||
@@ -178,6 +183,7 @@ export interface ContainerHit extends BaseContentHit {
|
||||
published?: ContentPublishedData;
|
||||
publishStatus: PublishStatus;
|
||||
formatted: BaseContentHit['formatted'] & { published?: ContentPublishedData, };
|
||||
content?: ContainerHitContent;
|
||||
}
|
||||
|
||||
export type HitType = ContentHit | CollectionHit | ContainerHit;
|
||||
|
||||
Reference in New Issue
Block a user