From 21cbf80f23b6f4a2d68ba4e0602d2411e2d412da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Tue, 22 Oct 2024 13:47:07 -0500 Subject: [PATCH] feat: Show published components on content picker (#1420) * feat: Show published components on content picker --------- Co-authored-by: Braden MacDonald --- .../LibraryAuthoringPage.tsx | 8 ++- .../LibraryRecentlyModified.tsx | 16 ++++- .../__mocks__/collection-search.json | 20 +++++++ .../__mocks__/library-search.json | 60 +++++++++++++++---- .../__mocks__/libraryComponentsMock.ts | 8 +++ .../collections/LibraryCollectionPage.tsx | 8 ++- src/library-authoring/common/context.tsx | 6 ++ .../ComponentInfoHeader.test.tsx | 6 +- .../component-info/ComponentInfoHeader.tsx | 3 +- .../component-info/ComponentPreview.tsx | 16 ++++- .../component-picker/ComponentPicker.test.tsx | 9 +++ .../component-picker/ComponentPicker.tsx | 8 ++- .../components/ComponentCard.test.tsx | 2 + .../components/ComponentCard.tsx | 15 ++--- src/library-authoring/data/api.ts | 6 +- src/library-authoring/data/apiHooks.ts | 8 +-- src/search-manager/data/api.ts | 19 +++++- 17 files changed, 179 insertions(+), 39 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index e546dff26b..d52cbbb014 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -148,6 +148,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage libraryData, isLoadingLibraryData, componentPickerMode, + showOnlyPublished, sidebarComponentInfo, openInfoSidebar, } = useLibraryContext(); @@ -212,6 +213,11 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage /> ) : undefined; + const extraFilter = [`context_key = "${libraryId}"`]; + if (showOnlyPublished) { + extraFilter.push('last_published IS NOT NULL'); + } + return (
@@ -230,7 +236,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage )} } diff --git a/src/library-authoring/LibraryRecentlyModified.tsx b/src/library-authoring/LibraryRecentlyModified.tsx index 5577d45877..6d67e5a554 100644 --- a/src/library-authoring/LibraryRecentlyModified.tsx +++ b/src/library-authoring/LibraryRecentlyModified.tsx @@ -56,11 +56,21 @@ const RecentlyModified: React.FC> = () => { }; const LibraryRecentlyModified: React.FC> = () => { - const { libraryId } = useLibraryContext(); + const { libraryId, showOnlyPublished } = useLibraryContext(); + + const extraFilter = [`context_key = "${libraryId}"`]; + if (showOnlyPublished) { + extraFilter.push('last_published IS NOT NULL'); + } + return ( diff --git a/src/library-authoring/__mocks__/collection-search.json b/src/library-authoring/__mocks__/collection-search.json index 7b4ddd3a40..dba7008aaf 100644 --- a/src/library-authoring/__mocks__/collection-search.json +++ b/src/library-authoring/__mocks__/collection-search.json @@ -34,6 +34,10 @@ "key": [ "my-first-collection" ] + }, + "published": { + "display_name": "Introduction to Testing", + "description": "Testing" } }, { @@ -64,6 +68,10 @@ "key": [ "my-first-collection" ] + }, + "published": { + "display_name": "Second Text Component", + "description": "Second Testing" } }, { @@ -97,6 +105,10 @@ "my-first-collection", "my-second-collection" ] + }, + "published": { + "display_name": "Third Text component", + "description": "Third Testing" } }, { @@ -128,6 +140,10 @@ "key": [ "my-first-collection" ] + }, + "published": { + "display_name": "Text 4", + "description": "Testing 4" } }, { @@ -159,6 +175,10 @@ "key": [ "my-first-collection" ] + }, + "published": { + "display_name": "Blank Problem", + "description": "Problem" } } ], diff --git a/src/library-authoring/__mocks__/library-search.json b/src/library-authoring/__mocks__/library-search.json index 72ea474fdd..aebfcb81ed 100644 --- a/src/library-authoring/__mocks__/library-search.json +++ b/src/library-authoring/__mocks__/library-search.json @@ -26,7 +26,11 @@ "block_type": "html", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Introduction to Testing", + "description": "Testing" + } }, { "id": "lbaximtesthtml73a22298-bcd9-4f4c-ae34-0bc2b0612480-46b4a7f2", @@ -48,7 +52,11 @@ "block_type": "html", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Second Text Component", + "description": "Second Testing" + } }, { "id": "lbaximtesthtmlbe5b5db9-26ba-4fac-86af-654538c70b5e-73dbaa95", @@ -71,7 +79,11 @@ "block_type": "html", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Third Text component", + "description": "Third Testing" + } }, { "id": "lbaximtesthtmle59e8c73-4056-4894-bca4-062781fb3f68-46a404b2", @@ -94,7 +106,11 @@ "block_type": "html", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Text 4", + "description": "Testing 4" + } }, { "id": "lbaximtestproblemf16116c9-516e-4bb9-b99e-103599f62417-f2798115", @@ -117,7 +133,11 @@ "block_type": "problem", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Blank Problem", + "description": "Problem" + } }, { "id": "lbaximtestproblem2ace6b9b-6620-413c-a66f-19c797527f34-3a7973b7", @@ -143,7 +163,11 @@ "block_type": "problem", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Multiple Choice Problem", + "description": "Problem" + } }, { "id": "lbaximtestproblem7d7e98ba-3ac9-4aa8-8946-159129b39a28-3a7973b7", @@ -169,7 +193,11 @@ "block_type": "problem", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Single Choice Problem", + "description": "Problem" + } }, { "id": "lbaximtestproblem4e1a72f9-ac93-42aa-a61c-ab5f9698c398-3a7973b7", @@ -195,7 +223,11 @@ "block_type": "problem", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Numerical Response Problem", + "description": "Problem" + } }, { "id": "lbaximtestproblemad483625-ade2-4712-88d8-c9743abbd291-3a7973b7", @@ -221,7 +253,11 @@ "block_type": "problem", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "Option Response Problem", + "description": "Problem" + } }, { "id": "lbaximtestproblemb4c859cb-de70-421a-917b-e6e01ce44bd8-3a7973b7", @@ -247,7 +283,11 @@ "block_type": "problem", "context_key": "lib:Axim:TEST", "org": "Axim", - "access_id": 15 + "access_id": 15, + "published": { + "display_name": "String Response Problem", + "description": "Problem" + } } ], "query": "", diff --git a/src/library-authoring/__mocks__/libraryComponentsMock.ts b/src/library-authoring/__mocks__/libraryComponentsMock.ts index 48a920392a..2f6f66fc61 100644 --- a/src/library-authoring/__mocks__/libraryComponentsMock.ts +++ b/src/library-authoring/__mocks__/libraryComponentsMock.ts @@ -3,11 +3,13 @@ export default [ id: '1', usageKey: 'lb:org:lib:html:1', displayName: 'Text Component 1', + description: 'This is a text: ID=1', formatted: { displayName: 'Text Component 1', content: { htmlContent: 'This is a text: ID=1', }, + description: 'This is a text: ID=1', }, tags: { level0: ['1', '2', '3'], @@ -18,11 +20,13 @@ export default [ id: '2', usageKey: 'lb:org:lib:html:2', displayName: 'Text Component 2', + description: 'This is a text: ID=2', formatted: { displayName: 'Text Component 2', content: { htmlContent: 'This is a text: ID=2', }, + description: 'This is a text: ID=2', }, tags: { level0: ['1', '2', '3'], @@ -60,11 +64,13 @@ export default [ id: '5', usageKey: 'lb:org:lib:problem:5', displayName: 'Problem', + description: 'This is a problem: ID=5', formatted: { displayName: 'Problem', content: { capaContent: 'This is a problem: ID=5', }, + description: 'This is a problem: ID=5', }, blockType: 'problem', }, @@ -72,11 +78,13 @@ export default [ id: '6', usageKey: 'lb:org:lib:problem:6', displayName: 'Problem', + description: 'This is a problem: ID=6', formatted: { displayName: 'Problem', content: { capaContent: 'This is a problem: ID=6', }, + description: 'This is a problem: ID=6', }, blockType: 'problem', }, diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 22692c5b5d..00b2967f77 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -107,6 +107,7 @@ const LibraryCollectionPage = () => { sidebarComponentInfo, openCollectionInfoSidebar, componentPickerMode, + showOnlyPublished, setCollectionId, } = useLibraryContext(); @@ -175,6 +176,11 @@ const LibraryCollectionPage = () => { /> ); + const extraFilter = [`context_key = "${libraryId}"`, `collections.key = "${collectionId}"`]; + if (showOnlyPublished) { + extraFilter.push('last_published IS NOT NULL'); + } + return (
@@ -189,7 +195,7 @@ const LibraryCollectionPage = () => { )} void; // Whether we're in "component picker" mode componentPickerMode: boolean; + // Only show published components + showOnlyPublished: boolean; // Sidebar stuff - only one sidebar is active at any given time: closeLibrarySidebar: () => void; openAddContentSidebar: () => void; @@ -79,6 +81,7 @@ interface LibraryProviderProps { /** The component picker mode is a special mode where the user is selecting a component to add to a Unit (or another * XBlock) */ componentPickerMode?: boolean; + showOnlyPublished?: boolean; /** Only used for testing */ initialSidebarComponentInfo?: SidebarComponentInfo; } @@ -91,6 +94,7 @@ export const LibraryProvider = ({ libraryId, collectionId: collectionIdProp, componentPickerMode = false, + showOnlyPublished = false, initialSidebarComponentInfo, }: LibraryProviderProps) => { const [collectionId, setCollectionId] = useState(collectionIdProp); @@ -148,6 +152,7 @@ export const LibraryProvider = ({ readOnly, isLoadingLibraryData, componentPickerMode, + showOnlyPublished, closeLibrarySidebar, openAddContentSidebar, openInfoSidebar, @@ -172,6 +177,7 @@ export const LibraryProvider = ({ readOnly, isLoadingLibraryData, componentPickerMode, + showOnlyPublished, closeLibrarySidebar, openAddContentSidebar, openInfoSidebar, diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx index fe55839859..832f0eebf0 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx @@ -8,7 +8,7 @@ import { initializeMocks, } from '../../testUtils'; import { mockContentLibrary } from '../data/api.mocks'; -import { getXBlockFieldsApiUrl } from '../data/api'; +import { getXBlockFieldsVersionApiUrl, getXBlockFieldsApiUrl } from '../data/api'; import { LibraryProvider, SidebarBodyComponentId } from '../common/context'; import ComponentInfoHeader from './ComponentInfoHeader'; @@ -45,7 +45,7 @@ describe('', () => { beforeEach(() => { const mocks = initializeMocks(); axiosMock = mocks.axiosMock; - axiosMock.onGet(getXBlockFieldsApiUrl(usageKey)).reply(200, xBlockFields); + axiosMock.onGet(getXBlockFieldsVersionApiUrl(usageKey, 'draft')).reply(200, xBlockFields); mockShowToast = mocks.mockShowToast; }); @@ -97,7 +97,7 @@ describe('', () => { }); it('should close edit library title on press Escape', async () => { - const url = getXBlockFieldsApiUrl(usageKey); + const url = getXBlockFieldsVersionApiUrl(usageKey, 'draft'); axiosMock.onPost(url).reply(200); render(); diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx index 5c27071255..295e4a3821 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx @@ -20,6 +20,7 @@ const ComponentInfoHeader = () => { const { sidebarComponentInfo, readOnly, + showOnlyPublished, } = useLibraryContext(); const usageKey = sidebarComponentInfo?.id; @@ -29,7 +30,7 @@ const ComponentInfoHeader = () => { } const { data: xblockFields, - } = useXBlockFields(usageKey); + } = useXBlockFields(usageKey, showOnlyPublished ? 'published' : 'draft'); const updateMutation = useUpdateXBlockFields(usageKey); const { showToast } = useContext(ToastContext); diff --git a/src/library-authoring/component-info/ComponentPreview.tsx b/src/library-authoring/component-info/ComponentPreview.tsx index a7448f3b8f..ba40e223e3 100644 --- a/src/library-authoring/component-info/ComponentPreview.tsx +++ b/src/library-authoring/component-info/ComponentPreview.tsx @@ -15,6 +15,7 @@ interface ModalComponentPreviewProps { const ModalComponentPreview = ({ isOpen, close, usageKey }: ModalComponentPreviewProps) => { const intl = useIntl(); + const { showOnlyPublished } = useLibraryContext(); return ( - + ); }; @@ -33,7 +37,7 @@ const ComponentPreview = () => { const intl = useIntl(); const [isModalOpen, openModal, closeModal] = useToggle(); - const { sidebarComponentInfo } = useLibraryContext(); + const { sidebarComponentInfo, showOnlyPublished } = useLibraryContext(); const usageKey = sidebarComponentInfo?.id; // istanbul ignore if: this should never happen @@ -58,7 +62,13 @@ const ComponentPreview = () => { { // key=modified below is used to auto-refresh the preview when changes are made, e.g. via OLX editor componentMetadata - ? + ? ( + + ) : null }
diff --git a/src/library-authoring/component-picker/ComponentPicker.test.tsx b/src/library-authoring/component-picker/ComponentPicker.test.tsx index 29ecca195f..6341dfe1b6 100644 --- a/src/library-authoring/component-picker/ComponentPicker.test.tsx +++ b/src/library-authoring/component-picker/ComponentPicker.test.tsx @@ -17,6 +17,15 @@ import { import { ComponentPicker } from './ComponentPicker'; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: '/evilguy', + search: { + variant: 'published', + }, + }), +})); mockContentLibrary.applyMock(); mockContentSearchConfig.applyMock(); mockGetCollectionMetadata.applyMock(); diff --git a/src/library-authoring/component-picker/ComponentPicker.tsx b/src/library-authoring/component-picker/ComponentPicker.tsx index 372506d4cd..2502e1ac6b 100644 --- a/src/library-authoring/component-picker/ComponentPicker.tsx +++ b/src/library-authoring/component-picker/ComponentPicker.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { Stepper } from '@openedx/paragon'; import { LibraryProvider, useLibraryContext } from '../common/context'; @@ -24,6 +25,11 @@ export const ComponentPicker = () => { const [currentStep, setCurrentStep] = useState('select-library'); const [selectedLibrary, setSelectedLibrary] = useState(''); + const location = useLocation(); + + const queryParams = new URLSearchParams(location.search); + const variant = queryParams.get('variant') || 'draft'; + const handleLibrarySelection = (library: string) => { setCurrentStep('pick-components'); setSelectedLibrary(library); @@ -43,7 +49,7 @@ export const ComponentPicker = () => { - + diff --git a/src/library-authoring/components/ComponentCard.test.tsx b/src/library-authoring/components/ComponentCard.test.tsx index f5a8266453..fb5fb9685d 100644 --- a/src/library-authoring/components/ComponentCard.test.tsx +++ b/src/library-authoring/components/ComponentCard.test.tsx @@ -19,11 +19,13 @@ const contentHit: ContentHit = { org: 'org1', breadcrumbs: [{ displayName: 'Demo Lib' }], displayName: 'Text Display Name', + description: 'This is a text: ID=1', formatted: { displayName: 'Text Display Formated Name', content: { htmlContent: 'This is a text: ID=1', }, + description: 'This is a text: ID=1', }, tags: { level0: ['1', '2', '3'], diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 2d61b858cc..f0da3a51a0 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -106,6 +106,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { const { openComponentInfoSidebar, componentPickerMode, + showOnlyPublished, } = useLibraryContext(); const { @@ -114,12 +115,12 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { tags, usageKey, } = contentHit; - const description: string = (/* eslint-disable */ - blockType === 'html' ? formatted?.content?.htmlContent : - blockType === 'problem' ? formatted?.content?.capaContent : - undefined - ) ?? '';/* eslint-enable */ - const displayName = formatted?.displayName ?? ''; + const componentDescription: string = ( + showOnlyPublished ? formatted.published?.description : formatted.description + ) ?? ''; + const displayName: string = ( + showOnlyPublished ? formatted.published?.displayName : formatted.displayName + ) ?? ''; const handleAddComponentToCourse = () => { window.parent.postMessage({ @@ -133,7 +134,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 25b162e58b..c47f4bdc57 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -52,6 +52,8 @@ export const getLibraryPasteClipboardUrl = (libraryId: string) => `${getApiBaseU * Get the URL for the xblock fields/metadata API. */ export const getXBlockFieldsApiUrl = (usageKey: string) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}/fields/`; +export const getXBlockFieldsVersionApiUrl = (usageKey: string, version: string) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}@${version}/fields/`; + /** * Get the URL for the xblock OLX API */ @@ -383,8 +385,8 @@ export async function getLibraryBlockMetadata(usageKey: string): Promise { - const { data } = await getAuthenticatedHttpClient().get(getXBlockFieldsApiUrl(usageKey)); +export async function getXBlockFields(usageKey: string, version: string = 'draft'): Promise { + const { data } = await getAuthenticatedHttpClient().get(getXBlockFieldsVersionApiUrl(usageKey, version)); return camelCaseObject(data); } diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 88a6702f73..baba2bcfcd 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -89,7 +89,7 @@ export const xblockQueryKeys = { */ xblock: (usageKey?: string) => [...xblockQueryKeys.all, usageKey], /** Fields (i.e. the content, display name, etc.) of an XBlock */ - xblockFields: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'fields'], + xblockFields: (usageKey: string, version: string = 'draft') => [...xblockQueryKeys.xblock(usageKey), 'fields', version], /** OLX (XML representation of the fields/content) */ xblockOLX: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'OLX'], /** assets (static files) */ @@ -290,10 +290,10 @@ export const useLibraryBlockMetadata = (usageId: string) => ( }) ); -export const useXBlockFields = (usageKey: string) => ( +export const useXBlockFields = (usageKey: string, version: string = 'draft') => ( useQuery({ - queryKey: xblockQueryKeys.xblockFields(usageKey), - queryFn: () => getXBlockFields(usageKey), + queryKey: xblockQueryKeys.xblockFields(usageKey, version), + queryFn: () => getXBlockFields(usageKey, version), enabled: !!usageKey, }) ); diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index b9ede51d5b..08bb0fd63b 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -127,9 +127,21 @@ export interface ContentHit extends BaseContentHit { * - After that is the name and usage key of any parent Section/Subsection/Unit/etc. */ breadcrumbs: [{ displayName: string }, ...Array<{ displayName: string, usageKey: string }>]; + description?: string; content?: ContentDetails; lastPublished: number | null; - collections: { displayName?: string[], key?: string[] }, + collections: { displayName?: string[], key?: string[] }; + published?: ContentPublishedData; + formatted: BaseContentHit['formatted'] & { published?: ContentPublishedData, }; +} + +/** + * Information about the published data of single Xblock returned in search results + * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py + */ +export interface ContentPublishedData { + description?: string, + displayName?: string, } /** @@ -152,6 +164,7 @@ export function formatSearchHit(hit: Record): ContentHit | Collecti displayName: _formatted?.display_name, content: _formatted?.content ?? {}, description: _formatted?.description, + published: _formatted?.published, }; return camelCaseObject(newHit); } @@ -247,10 +260,10 @@ export async function fetchSearchResults({ ...extraFilterFormatted, ...tagsFilterFormatted, ], - attributesToHighlight: ['display_name', 'content'], + attributesToHighlight: ['display_name', 'description', 'published'], highlightPreTag: HIGHLIGHT_PRE_TAG, highlightPostTag: HIGHLIGHT_POST_TAG, - attributesToCrop: ['content'], + attributesToCrop: ['description', 'published'], sort, offset, limit,