Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/add-content/AddContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
9 changes: 8 additions & 1 deletion src/library-authoring/add-content/AddContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 && (
Expand All @@ -123,6 +128,8 @@ const AddContentView = ({
<PickLibraryContentModal
isOpen={isAddLibraryContentModalOpen}
onClose={closeAddLibraryContentModal}
extraFilter={extraFilter}
visibleTabs={visibleTabs}
/>
</>
)}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
});
11 changes: 8 additions & 3 deletions src/library-authoring/add-content/PickLibraryContentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -109,6 +113,7 @@ export const PickLibraryContentModal: React.FC<PickLibraryContentModalProps> = (
componentPickerMode="multiple"
onChangeComponentSelection={setSelectedComponents}
extraFilter={extraFilter}
visibleTabs={visibleTabs}
/>
</StandardModal>
);
Expand Down
11 changes: 3 additions & 8 deletions src/library-authoring/add-content/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/common/context/SidebarContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 33 additions & 9 deletions src/library-authoring/components/ContainerCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }) => (
Expand Down Expand Up @@ -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();
});
});
30 changes: 17 additions & 13 deletions src/library-authoring/components/ContainerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand All @@ -133,7 +130,7 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar
<Icon
src={getItemIcon(blockType)}
screenReaderText={blockType}
title={displayName}
title={usageKey}
/>
);
} else {
Expand All @@ -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}
Expand Down Expand Up @@ -176,6 +175,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
published,
publishStatus,
usageKey: unitId,
content,
} = hit;

const numChildrenCount = showOnlyPublished ? (
Expand All @@ -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(() => {
Expand All @@ -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={(
Expand Down
4 changes: 2 additions & 2 deletions src/library-authoring/containers/ContainerOrganize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand Down Expand Up @@ -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} />
Expand Down
4 changes: 2 additions & 2 deletions src/library-authoring/containers/UnitInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down Expand Up @@ -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>
Expand Down
20 changes: 10 additions & 10 deletions src/library-authoring/containers/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading