fix: only show library actions available for the user (#2712)

* fix: only show the options available for the user

* test: fix and add tests

* fix: improve following the best practices

* fix: apply the changes for collections and containers
This commit is contained in:
María Fernanda Magallanes
2025-12-17 19:49:54 -05:00
committed by GitHub
parent 4df44ab6cf
commit 9c70fd9216
6 changed files with 125 additions and 29 deletions

View File

@@ -1,6 +1,7 @@
import userEvent from '@testing-library/user-event';
import type MockAdapter from 'axios-mock-adapter';
import { mockContentLibrary } from '@src/library-authoring/data/api.mocks';
import {
initializeMocks, render as baseRender, screen, waitFor, within, fireEvent,
} from '../../testUtils';
@@ -43,14 +44,18 @@ const collectionHitSample: CollectionHit = {
let axiosMock: MockAdapter;
let mockShowToast;
const libraryId = 'lib:org1:Demo_Course';
const { libraryId } = mockContentLibrary;
const render = (ui: React.ReactElement, showOnlyPublished: boolean = false) => baseRender(ui, {
const render = (
ui: React.ReactElement,
showOnlyPublished: boolean = false,
libId: string = libraryId,
) => baseRender(ui, {
path: '/library/:libraryId',
params: { libraryId },
params: { libraryId: libId },
extraWrapper: ({ children }) => (
<LibraryProvider
libraryId="lib:Axim:TEST"
libraryId={libId}
showOnlyPublished={showOnlyPublished}
>
{children}
@@ -63,6 +68,7 @@ describe('<CollectionCard />', () => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
mockContentLibrary.applyMock();
});
it('should render the card with title and description', () => {
@@ -193,4 +199,24 @@ describe('<CollectionCard />', () => {
expect(mockShowToast).toHaveBeenCalledWith('Failed to delete collection');
});
});
it('should not show delete button when library is read-only', async () => {
const user = userEvent.setup();
// Render with read-only library
render(<CollectionCard hit={collectionHitSample} />, false, mockContentLibrary.libraryIdReadOnly);
// Open menu
const menu = await screen.findByTestId('collection-card-menu-toggle');
expect(menu).toBeInTheDocument();
await user.click(menu);
// Delete button should not be visible in readonly mode
const deleteOption = screen.queryByRole('button', { name: 'Delete' });
expect(deleteOption).not.toBeInTheDocument();
// Open button should still be visible
const openOption = screen.queryByRole('button', { name: 'Open' });
expect(openOption).toBeInTheDocument();
});
});

View File

@@ -28,6 +28,7 @@ const CollectionMenu = ({ hit } : CollectionMenuProps) => {
const intl = useIntl();
const { showToast } = useContext(ToastContext);
const { navigateTo } = useLibraryRoutes();
const { readOnly } = useLibraryContext();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const { closeLibrarySidebar, sidebarItemInfo } = useSidebarContext();
const {
@@ -90,9 +91,11 @@ const CollectionMenu = ({ hit } : CollectionMenuProps) => {
<Dropdown.Item onClick={openCollection}>
<FormattedMessage {...messages.menuOpen} />
</Dropdown.Item>
<Dropdown.Item onClick={openDeleteModal}>
<FormattedMessage {...messages.deleteCollection} />
</Dropdown.Item>
{!readOnly && (
<Dropdown.Item onClick={openDeleteModal}>
<FormattedMessage {...messages.deleteCollection} />
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
<DeleteModal

View File

@@ -1,3 +1,4 @@
import { mockContentLibrary } from '@src/library-authoring/data/api.mocks';
import {
fireEvent,
render as baseRender,
@@ -48,12 +49,12 @@ const contentHit: ContentHit = {
publishStatus: PublishStatus.Published,
};
const libraryId = 'lib:org1:Demo_Course';
const render = () => baseRender(<ComponentCard hit={contentHit} />, {
const { libraryId } = mockContentLibrary;
const render = (libId: string = libraryId) => baseRender(<ComponentCard hit={contentHit} />, {
path: '/library/:libraryId',
params: { libraryId },
params: { libraryId: libId },
extraWrapper: ({ children }) => (
<LibraryProvider libraryId={libraryId}>
<LibraryProvider libraryId={libId}>
<SidebarProvider>
{ children }
</SidebarProvider>
@@ -62,6 +63,9 @@ const render = () => baseRender(<ComponentCard hit={contentHit} />, {
});
describe('<ComponentCard />', () => {
beforeEach(() => {
mockContentLibrary.applyMock();
});
it('should render the card with title and description', () => {
initializeMocks();
render();
@@ -127,7 +131,7 @@ describe('<ComponentCard />', () => {
expect(menu).toBeInTheDocument();
fireEvent.click(menu);
// Click copy to clipboard
// Click edit option
const editOption = await screen.findByRole('button', { name: 'Edit' });
expect(editOption).toBeInTheDocument();
fireEvent.click(editOption);
@@ -137,4 +141,18 @@ describe('<ComponentCard />', () => {
search: '',
});
});
it('should not show edit button when library is read-only', async () => {
initializeMocks();
render(mockContentLibrary.libraryIdReadOnly);
// Open menu
const menu = await screen.findByTestId('component-card-menu-toggle');
expect(menu).toBeInTheDocument();
fireEvent.click(menu);
// Edit button should not be visible in readonly mode
const editOption = screen.queryByRole('button', { name: 'Edit' });
expect(editOption).not.toBeInTheDocument();
});
});

View File

@@ -35,6 +35,7 @@ export const ComponentMenu = ({ usageKey, index }: Props) => {
collectionId,
containerId,
openComponentEditor,
readOnly,
} = useLibraryContext();
const {
@@ -103,9 +104,11 @@ export const ComponentMenu = ({ usageKey, index }: Props) => {
data-testid="component-card-menu-toggle"
/>
<Dropdown.Menu>
<Dropdown.Item {...(canEdit ? { onClick: handleEdit } : { disabled: true })}>
<FormattedMessage {...messages.menuEdit} />
</Dropdown.Item>
{!readOnly && (
<Dropdown.Item {...(canEdit ? { onClick: handleEdit } : { disabled: true })}>
<FormattedMessage {...messages.menuEdit} />
</Dropdown.Item>
)}
<Dropdown.Item onClick={updateClipboardClick}>
<FormattedMessage {...messages.menuCopyToClipboard} />
</Dropdown.Item>
@@ -114,10 +117,12 @@ export const ComponentMenu = ({ usageKey, index }: Props) => {
<FormattedMessage {...messages.removeComponentFromUnitMenu} />
</Dropdown.Item>
)}
<Dropdown.Item onClick={openDeleteModal}>
<FormattedMessage {...messages.menuDelete} />
</Dropdown.Item>
{insideCollection && (
{!readOnly && (
<Dropdown.Item onClick={openDeleteModal}>
<FormattedMessage {...messages.menuDelete} />
</Dropdown.Item>
)}
{insideCollection && !readOnly && (
<Dropdown.Item onClick={removeFromCollection}>
<FormattedMessage
{...containerMessages.menuRemoveFromContainer}
@@ -127,9 +132,11 @@ export const ComponentMenu = ({ usageKey, index }: Props) => {
/>
</Dropdown.Item>
)}
<Dropdown.Item onClick={showManageCollections}>
<FormattedMessage {...containerMessages.menuAddToCollection} />
</Dropdown.Item>
{!readOnly && (
<Dropdown.Item onClick={showManageCollections}>
<FormattedMessage {...containerMessages.menuAddToCollection} />
</Dropdown.Item>
)}
</Dropdown.Menu>
{isDeleteModalOpen && (
<ComponentDeleter

View File

@@ -493,4 +493,40 @@ describe('<ContainerCard />', () => {
});
expect(axiosMock.history.post[0].url).toEqual(url);
});
test.each([
ContainerType.Unit,
ContainerType.Subsection,
ContainerType.Section,
])('should not show delete and add to collection buttons when library is read-only for %s', async (containerType) => {
const containerHit = getContainerHitSample(containerType);
const user = userEvent.setup();
// Render with read-only library
baseRender(<ContainerCard hit={containerHit} />, {
path: '/library/:libraryId',
params: { libraryId: mockContentLibrary.libraryIdReadOnly },
extraWrapper: ({ children }) => (
<LibraryProvider libraryId={mockContentLibrary.libraryIdReadOnly}>
{children}
</LibraryProvider>
),
});
// Open menu
const menu = await screen.findByTestId('container-card-menu-toggle');
expect(menu).toBeInTheDocument();
await user.click(menu);
// Delete and Add to collection buttons should not be visible in readonly mode
const deleteOption = screen.queryByRole('button', { name: 'Delete' });
expect(deleteOption).not.toBeInTheDocument();
const addToCollectionOption = screen.queryByRole('button', { name: 'Add to collection' });
expect(addToCollectionOption).not.toBeInTheDocument();
// Copy button should still be visible
const copyOption = screen.queryByRole('button', { name: 'Copy to clipboard' });
expect(copyOption).toBeInTheDocument();
});
});

View File

@@ -36,7 +36,9 @@ type ContainerMenuProps = {
export const ContainerMenu = ({ containerKey, displayName, index } : ContainerMenuProps) => {
const intl = useIntl();
const { libraryId, collectionId, containerId } = useLibraryContext();
const {
libraryId, collectionId, containerId, readOnly,
} = useLibraryContext();
const {
sidebarItemInfo,
closeLibrarySidebar,
@@ -116,9 +118,11 @@ export const ContainerMenu = ({ containerKey, displayName, index } : ContainerMe
<Dropdown.Item onClick={handleCopy}>
<FormattedMessage {...messages.menuCopyContainer} />
</Dropdown.Item>
<Dropdown.Item onClick={confirmDelete}>
<FormattedMessage {...messages.menuDeleteContainer} />
</Dropdown.Item>
{!readOnly && (
<Dropdown.Item onClick={confirmDelete}>
<FormattedMessage {...messages.menuDeleteContainer} />
</Dropdown.Item>
)}
{(insideCollection || insideSection || insideSubsection) && (
<Dropdown.Item onClick={handleRemove}>
<FormattedMessage
@@ -129,9 +133,11 @@ export const ContainerMenu = ({ containerKey, displayName, index } : ContainerMe
/>
</Dropdown.Item>
)}
<Dropdown.Item onClick={showManageCollections}>
<FormattedMessage {...messages.menuAddToCollection} />
</Dropdown.Item>
{!readOnly && (
<Dropdown.Item onClick={showManageCollections}>
<FormattedMessage {...messages.menuAddToCollection} />
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
{isConfirmingDelete && (