fix: Issue with read-only units in libraries & published version of units in library units picker [FC-0083] (#1926)

Fixes the issues from https://github.com/openedx/frontend-app-authoring/issues/1633#issuecomment-2828953801

* In successfully added units, the "add new component" widget appears sometimes
* In the "add existing unit" modal, the preview shows draft versions of units
This commit is contained in:
Chris Chávez
2025-05-12 09:45:46 -05:00
committed by GitHub
parent 9a2dc8061a
commit 7b2cc125a5
8 changed files with 45 additions and 16 deletions

View File

@@ -3,7 +3,7 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { PUBLISH_TYPES } from '../constants';
import { normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
@@ -24,7 +24,9 @@ export async function getCourseUnitData(unitId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseUnitApiUrl(unitId));
return camelCaseObject(data);
const result = camelCaseObject(data);
result.readOnly = isUnitReadOnly(result);
return result;
}
/**

View File

@@ -38,7 +38,7 @@ import {
updateCourseOutlineInfoLoadingStatus,
updateMovedXBlockParams,
} from './slice';
import { getNotificationMessage, isUnitReadOnly } from './utils';
import { getNotificationMessage } from './utils';
export function fetchCourseUnitQuery(courseId) {
return async (dispatch) => {
@@ -46,7 +46,6 @@ export function fetchCourseUnitQuery(courseId) {
try {
const courseUnit = await getCourseUnitData(courseId);
courseUnit.readOnly = isUnitReadOnly(courseUnit);
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));

View File

@@ -208,7 +208,9 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
if (itemType === 'unit') {
openUnitInfoSidebar(unitId);
setUnitId(unitId);
navigateTo({ unitId });
if (!componentPickerMode) {
navigateTo({ unitId });
}
}
}, [unitId, itemType, openUnitInfoSidebar, navigateTo]);

View File

@@ -5,7 +5,7 @@ import {
initializeMocks, render as baseRender, screen, waitFor,
fireEvent,
} from '../../testUtils';
import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks';
import { mockContentLibrary, mockGetContainerChildren, mockGetContainerMetadata } from '../data/api.mocks';
import { LibraryProvider } from '../common/context/LibraryContext';
import UnitInfo from './UnitInfo';
import { getLibraryContainerApiUrl, getLibraryContainerPublishApiUrl } from '../data/api';
@@ -14,14 +14,16 @@ import { SidebarBodyComponentId, SidebarProvider } from '../common/context/Sideb
mockGetContainerMetadata.applyMock();
mockContentLibrary.applyMock();
mockGetContainerMetadata.applyMock();
mockGetContainerChildren.applyMock();
const { libraryId } = mockContentLibrary;
const { containerId } = mockGetContainerMetadata;
const render = () => baseRender(<UnitInfo />, {
const render = (showOnlyPublished: boolean = false) => baseRender(<UnitInfo />, {
extraWrapper: ({ children }) => (
<LibraryProvider
libraryId={libraryId}
showOnlyPublished={showOnlyPublished}
>
<SidebarProvider
initialSidebarComponentInfo={{
@@ -95,4 +97,10 @@ describe('<UnitInfo />', () => {
});
expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes');
});
it('show only published content', async () => {
render(true);
expect(await screen.findByTestId('unit-info-menu-toggle')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /text block published 1/i })).toBeInTheDocument();
});
});

View File

@@ -188,6 +188,7 @@ mockCreateLibraryBlock.newHtmlData = {
id: 'lb:Axim:TEST:html:123',
blockType: 'html',
displayName: 'New Text Component',
publishedDisplayName: null,
hasUnpublishedChanges: true,
lastPublished: null, // or e.g. '2024-08-30T16:37:42Z',
publishedBy: null, // or e.g. 'test_author',
@@ -202,6 +203,7 @@ mockCreateLibraryBlock.newProblemData = {
id: 'lb:Axim:TEST:problem:prob1',
blockType: 'problem',
displayName: 'New Problem',
publishedDisplayName: null,
hasUnpublishedChanges: true,
lastPublished: null, // or e.g. '2024-08-30T16:37:42Z',
publishedBy: null, // or e.g. 'test_author',
@@ -216,6 +218,7 @@ mockCreateLibraryBlock.newVideoData = {
id: 'lb:Axim:TEST:video:vid1',
blockType: 'video',
displayName: 'New Video',
publishedDisplayName: null,
hasUnpublishedChanges: true,
lastPublished: null, // or e.g. '2024-08-30T16:37:42Z',
publishedBy: null, // or e.g. 'test_author',
@@ -348,6 +351,7 @@ mockLibraryBlockMetadata.dataNeverPublished = {
id: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1',
blockType: 'html',
displayName: 'Introduction to Testing 1',
publishedDisplayName: null,
lastPublished: null,
publishedBy: null,
lastDraftCreated: null,
@@ -363,6 +367,7 @@ mockLibraryBlockMetadata.dataPublished = {
id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2',
blockType: 'html',
displayName: 'Introduction to Testing 2',
publishedDisplayName: 'Introduction to Testing 2',
lastPublished: '2024-06-22T00:00:00',
publishedBy: 'Luke',
lastDraftCreated: null,
@@ -391,6 +396,7 @@ mockLibraryBlockMetadata.dataWithCollections = {
id: 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd',
blockType: 'html',
displayName: 'Introduction to Testing 2',
publishedDisplayName: null,
lastPublished: '2024-06-21T00:00:00',
publishedBy: 'Luke',
lastDraftCreated: null,
@@ -407,6 +413,7 @@ mockLibraryBlockMetadata.dataPublishedWithChanges = {
id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fvv',
blockType: 'html',
displayName: 'Introduction to Testing 2',
publishedDisplayName: 'Introduction to Testing 3',
lastPublished: '2024-06-22T00:00:00',
publishedBy: 'Luke',
lastDraftCreated: null,
@@ -536,6 +543,7 @@ export async function mockGetContainerChildren(containerId: string): Promise<api
// Generate a unique ID for each child block to avoid "duplicate key" errors in tests
id: `lb:org1:Demo_course:html:text-${idx}`,
displayName: `text block ${idx}`,
publishedDisplayName: `text block published ${idx}`,
}
)),
);
@@ -546,6 +554,7 @@ mockGetContainerChildren.childTemplate = {
id: 'lb:org1:Demo_course:html:text',
blockType: 'html',
displayName: 'text block',
publishedDisplayName: 'text block published',
lastPublished: null,
publishedBy: null,
lastDraftCreated: null,

View File

@@ -119,7 +119,7 @@ export const getLibraryContainerRestoreApiUrl = (containerId: string) => `${getL
/**
* Get the URL for a single container children api.
*/
export const getLibraryContainerChildrenApiUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}children/`;
export const getLibraryContainerChildrenApiUrl = (containerId: string, published: boolean = false) => `${getLibraryContainerApiUrl(containerId)}children/?published=${published}`;
/**
* Get the URL for library container collections.
*/
@@ -250,6 +250,7 @@ export interface LibraryBlockMetadata {
id: string;
blockType: string;
displayName: string;
publishedDisplayName: string | null;
lastPublished: string | null;
publishedBy: string | null;
lastDraftCreated: string | null;
@@ -652,8 +653,13 @@ export async function restoreContainer(containerId: string) {
/**
* Fetch a library container's children's metadata.
*/
export async function getLibraryContainerChildren(containerId: string): Promise<LibraryBlockMetadata[]> {
const { data } = await getAuthenticatedHttpClient().get(getLibraryContainerChildrenApiUrl(containerId));
export async function getLibraryContainerChildren(
containerId: string,
published: boolean = false,
): Promise<LibraryBlockMetadata[]> {
const { data } = await getAuthenticatedHttpClient().get(
getLibraryContainerChildrenApiUrl(containerId, published),
);
return camelCaseObject(data);
}

View File

@@ -641,11 +641,11 @@ export const useRestoreContainer = (containerId: string) => {
/**
* Get the metadata and children for a container in a library
*/
export const useContainerChildren = (containerId?: string) => (
export const useContainerChildren = (containerId?: string, published: boolean = false) => (
useQuery({
enabled: !!containerId,
queryKey: libraryAuthoringQueryKeys.containerChildren(containerId!),
queryFn: () => api.getLibraryContainerChildren(containerId!),
queryFn: () => api.getLibraryContainerChildren(containerId!, published),
structuralSharing: (oldData: api.LibraryBlockMetadata[], newData: api.LibraryBlockMetadata[]) => {
// This just sets `isNew` flag to new children components
if (oldData) {

View File

@@ -56,6 +56,7 @@ interface ComponentBlockProps {
/** Component header */
const BlockHeader = ({ block }: ComponentBlockProps) => {
const intl = useIntl();
const { showOnlyPublished } = useLibraryContext();
const { showToast } = useContext(ToastContext);
const { navigateTo } = useLibraryRoutes();
const { openComponentInfoSidebar, setSidebarAction } = useSidebarContext();
@@ -101,7 +102,7 @@ const BlockHeader = ({ block }: ComponentBlockProps) => {
<Icon src={getItemIcon(block.blockType)} />
<InplaceTextEditor
onSave={handleSaveDisplayName}
text={block.displayName}
text={showOnlyPublished ? (block.publishedDisplayName ?? block.displayName) : block.displayName}
/>
</Stack>
<ActionRow.Spacer />
@@ -112,7 +113,7 @@ const BlockHeader = ({ block }: ComponentBlockProps) => {
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
onClick={(e) => e.stopPropagation()}
>
{block.hasUnpublishedChanges && (
{!showOnlyPublished && block.hasUnpublishedChanges && (
<Badge
className="px-2 py-1"
variant="warning"
@@ -237,7 +238,9 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
const [hidePreviewFor, setHidePreviewFor] = useState<string | null>(null);
const { showToast } = useContext(ToastContext);
const { unitId, readOnly } = useLibraryContext();
const { readOnly, showOnlyPublished } = useLibraryContext();
const { sidebarComponentInfo } = useSidebarContext();
const unitId = sidebarComponentInfo?.id;
const { openAddContentSidebar } = useSidebarContext();
@@ -247,7 +250,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
isLoading,
isError,
error,
} = useContainerChildren(unitId);
} = useContainerChildren(unitId, showOnlyPublished);
const handleReorder = useCallback(() => async (newOrder?: LibraryBlockMetadataWithUniqueId[]) => {
if (!newOrder) {