From ac85a40f0dd288fb84f39f40cee3e42da17faf9a Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 31 Jul 2024 14:58:09 -0700 Subject: [PATCH] feat: In libraries, allow opening the editor for text (html) components --- src/CourseAuthoringRoutes.jsx | 2 +- src/editors/EditorContainer.jsx | 28 --------- src/editors/EditorContainer.test.jsx | 2 +- src/editors/EditorContainer.tsx | 42 ++++++++++++++ src/library-authoring/LibraryLayout.tsx | 57 +++++++++++++++++-- .../components/ComponentCard.tsx | 24 ++++++-- src/library-authoring/data/apiHooks.ts | 25 +++++++- 7 files changed, 136 insertions(+), 44 deletions(-) delete mode 100644 src/editors/EditorContainer.jsx create mode 100644 src/editors/EditorContainer.tsx diff --git a/src/CourseAuthoringRoutes.jsx b/src/CourseAuthoringRoutes.jsx index 51599317e6..0c9d2a1680 100644 --- a/src/CourseAuthoringRoutes.jsx +++ b/src/CourseAuthoringRoutes.jsx @@ -88,7 +88,7 @@ const CourseAuthoringRoutes = () => { /> } + element={} /> { - const { blockType, blockId } = useParams(); - return ( -
- -
- ); -}; -EditorContainer.propTypes = { - courseId: PropTypes.string.isRequired, -}; - -export default EditorContainer; diff --git a/src/editors/EditorContainer.test.jsx b/src/editors/EditorContainer.test.jsx index a6186050ae..d57d14c6b1 100644 --- a/src/editors/EditorContainer.test.jsx +++ b/src/editors/EditorContainer.test.jsx @@ -10,7 +10,7 @@ jest.mock('react-router', () => ({ }), })); -const props = { courseId: 'cOuRsEId' }; +const props = { learningContextId: 'cOuRsEId' }; describe('Editor Container', () => { describe('snapshots', () => { diff --git a/src/editors/EditorContainer.tsx b/src/editors/EditorContainer.tsx new file mode 100644 index 0000000000..aaa8680b61 --- /dev/null +++ b/src/editors/EditorContainer.tsx @@ -0,0 +1,42 @@ +/* eslint-disable react/require-default-props */ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { getConfig } from '@edx/frontend-platform'; + +import EditorPage from './EditorPage'; + +interface Props { + /** Course ID or Library ID */ + learningContextId: string; + /** Event handler for when user cancels out of the editor page */ + onClose?: () => void; + /** Event handler for when user saves changes using an editor */ + onSave?: () => (newData: Record) => void; +} + +const EditorContainer: React.FC = ({ + learningContextId, + onClose, + onSave, +}) => { + const { blockType, blockId } = useParams(); + if (blockType === undefined || blockId === undefined) { + // This shouldn't be possible; it's just here to satisfy the type checker. + return
Error: missing URL parameters
; + } + return ( +
+ +
+ ); +}; + +export default EditorContainer; diff --git a/src/library-authoring/LibraryLayout.tsx b/src/library-authoring/LibraryLayout.tsx index 95d829606f..65ad125b92 100644 --- a/src/library-authoring/LibraryLayout.tsx +++ b/src/library-authoring/LibraryLayout.tsx @@ -1,11 +1,58 @@ import React from 'react'; +import { + Route, + Routes, + useNavigate, + useParams, +} from 'react-router-dom'; +import { PageWrap } from '@edx/frontend-platform/react'; +import { useQueryClient } from '@tanstack/react-query'; + +import EditorContainer from '../editors/EditorContainer'; import LibraryAuthoringPage from './LibraryAuthoringPage'; import { LibraryProvider } from './common/context'; +import { invalidateComponentData } from './data/apiHooks'; + +const LibraryLayout = () => { + const { libraryId } = useParams(); + const queryClient = useQueryClient(); + + if (libraryId === undefined) { + throw new Error('Error: route is missing libraryId.'); // Should never happen + } + + const navigate = useNavigate(); + const goBack = React.useCallback(() => { + if (window.history.length > 1) { + navigate(-1); // go back + } else { + navigate(`/library/${libraryId}`); + } + // The following function is called only if changes are saved: + return ({ id: usageKey }) => { + // invalidate any queries that involve this XBlock: + invalidateComponentData(queryClient, libraryId, usageKey); + }; + }, []); -const LibraryLayout = () => ( - - - -); + return ( + + + + + + )} + /> + } + /> + + + ); +}; export default LibraryLayout; diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index f460bc3ba4..aca18c4521 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -1,5 +1,5 @@ import React, { useContext, useMemo, useState } from 'react'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, Card, @@ -10,6 +10,7 @@ import { Stack, } from '@openedx/paragon'; import { MoreVert } from '@openedx/paragon/icons'; +import { Link, useParams } from 'react-router-dom'; import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; import { updateClipboard } from '../../generic/data/api'; @@ -27,6 +28,9 @@ type ComponentCardProps = { export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const intl = useIntl(); + // Get the block type (e.g. "html") from a lib block usage key string like "lb:org:lib:block_type:id" + const blockType: string = usageKey.split(':')[3] ?? 'unknown'; + const { libraryId } = useParams(); const { showToast } = useContext(ToastContext); const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL)); const updateClipboardClick = () => { @@ -50,14 +54,22 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { data-testid="component-card-menu-toggle" /> - - {intl.formatMessage(messages.menuEdit)} - + { + blockType === 'html' ? ( + + + + ) : ( + + + + ) + } - {intl.formatMessage(messages.menuCopyToClipboard)} + - {intl.formatMessage(messages.menuAddToCollection)} + diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 2ebed19ff9..ca8bd92b8a 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -1,6 +1,10 @@ import { camelCaseObject } from '@edx/frontend-platform'; import { - useQuery, useMutation, useQueryClient, type Query, + useQuery, + useMutation, + useQueryClient, + type Query, + type QueryClient, } from '@tanstack/react-query'; import { @@ -61,6 +65,22 @@ export const libraryAuthoringQueryKeys = { ], }; +/** + * Tell react-query to refresh its cache of any data related to the given + * component (XBlock). + * + * Note that technically it's possible to derive the library key from the + * usageKey, so we could refactor this to only require the usageKey. + * + * @param queryClient The query client - get it via useQueryClient() + * @param contentLibraryId The ID of library that holds the XBlock ("lib:...") + * @param usageKey The usage ID of the XBlock ("lb:...") + */ +export function invalidateComponentData(queryClient: QueryClient, contentLibraryId: string, usageKey: string) { + queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey) }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) }); +} + /** * Hook to fetch a content library by its ID. */ @@ -204,8 +224,7 @@ export const useUpdateXBlockFields = (contentLibraryId: string, usageKey: string ); }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey) }); - queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) }); + invalidateComponentData(queryClient, contentLibraryId, usageKey); }, }); };