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:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user