Skip to content

Commit ac85a40

Browse files
feat: In libraries, allow opening the editor for text (html) components
1 parent dde8385 commit ac85a40

File tree

7 files changed

+136
-44
lines changed

7 files changed

+136
-44
lines changed

src/CourseAuthoringRoutes.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const CourseAuthoringRoutes = () => {
8888
/>
8989
<Route
9090
path="editor/:blockType/:blockId?"
91-
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
91+
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
9292
/>
9393
<Route
9494
path="settings/details"

src/editors/EditorContainer.jsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/editors/EditorContainer.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jest.mock('react-router', () => ({
1010
}),
1111
}));
1212

13-
const props = { courseId: 'cOuRsEId' };
13+
const props = { learningContextId: 'cOuRsEId' };
1414

1515
describe('Editor Container', () => {
1616
describe('snapshots', () => {

src/editors/EditorContainer.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-disable react/require-default-props */
2+
import React from 'react';
3+
import { useParams } from 'react-router-dom';
4+
import { getConfig } from '@edx/frontend-platform';
5+
6+
import EditorPage from './EditorPage';
7+
8+
interface Props {
9+
/** Course ID or Library ID */
10+
learningContextId: string;
11+
/** Event handler for when user cancels out of the editor page */
12+
onClose?: () => void;
13+
/** Event handler for when user saves changes using an editor */
14+
onSave?: () => (newData: Record<string, any>) => void;
15+
}
16+
17+
const EditorContainer: React.FC<Props> = ({
18+
learningContextId,
19+
onClose,
20+
onSave,
21+
}) => {
22+
const { blockType, blockId } = useParams();
23+
if (blockType === undefined || blockId === undefined) {
24+
// This shouldn't be possible; it's just here to satisfy the type checker.
25+
return <div>Error: missing URL parameters</div>;
26+
}
27+
return (
28+
<div className="editor-page">
29+
<EditorPage
30+
courseId={learningContextId}
31+
blockType={blockType}
32+
blockId={blockId}
33+
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
34+
lmsEndpointUrl={getConfig().LMS_BASE_URL}
35+
onClose={onClose}
36+
returnFunction={onSave}
37+
/>
38+
</div>
39+
);
40+
};
41+
42+
export default EditorContainer;
Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,58 @@
11
import React from 'react';
2+
import {
3+
Route,
4+
Routes,
5+
useNavigate,
6+
useParams,
7+
} from 'react-router-dom';
8+
import { PageWrap } from '@edx/frontend-platform/react';
9+
import { useQueryClient } from '@tanstack/react-query';
10+
11+
import EditorContainer from '../editors/EditorContainer';
212
import LibraryAuthoringPage from './LibraryAuthoringPage';
313
import { LibraryProvider } from './common/context';
14+
import { invalidateComponentData } from './data/apiHooks';
15+
16+
const LibraryLayout = () => {
17+
const { libraryId } = useParams();
18+
const queryClient = useQueryClient();
19+
20+
if (libraryId === undefined) {
21+
throw new Error('Error: route is missing libraryId.'); // Should never happen
22+
}
23+
24+
const navigate = useNavigate();
25+
const goBack = React.useCallback(() => {
26+
if (window.history.length > 1) {
27+
navigate(-1); // go back
28+
} else {
29+
navigate(`/library/${libraryId}`);
30+
}
31+
// The following function is called only if changes are saved:
32+
return ({ id: usageKey }) => {
33+
// invalidate any queries that involve this XBlock:
34+
invalidateComponentData(queryClient, libraryId, usageKey);
35+
};
36+
}, []);
437

5-
const LibraryLayout = () => (
6-
<LibraryProvider>
7-
<LibraryAuthoringPage />
8-
</LibraryProvider>
9-
);
38+
return (
39+
<LibraryProvider>
40+
<Routes>
41+
<Route
42+
path="editor/:blockType/:blockId?"
43+
element={(
44+
<PageWrap>
45+
<EditorContainer learningContextId={libraryId} onClose={goBack} onSave={goBack} />
46+
</PageWrap>
47+
)}
48+
/>
49+
<Route
50+
path="*"
51+
element={<LibraryAuthoringPage />}
52+
/>
53+
</Routes>
54+
</LibraryProvider>
55+
);
56+
};
1057

1158
export default LibraryLayout;

src/library-authoring/components/ComponentCard.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useContext, useMemo, useState } from 'react';
2-
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
33
import {
44
ActionRow,
55
Card,
@@ -10,6 +10,7 @@ import {
1010
Stack,
1111
} from '@openedx/paragon';
1212
import { MoreVert } from '@openedx/paragon/icons';
13+
import { Link, useParams } from 'react-router-dom';
1314

1415
import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils';
1516
import { updateClipboard } from '../../generic/data/api';
@@ -27,6 +28,9 @@ type ComponentCardProps = {
2728

2829
export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
2930
const intl = useIntl();
31+
// Get the block type (e.g. "html") from a lib block usage key string like "lb:org:lib:block_type:id"
32+
const blockType: string = usageKey.split(':')[3] ?? 'unknown';
33+
const { libraryId } = useParams();
3034
const { showToast } = useContext(ToastContext);
3135
const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL));
3236
const updateClipboardClick = () => {
@@ -50,14 +54,22 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
5054
data-testid="component-card-menu-toggle"
5155
/>
5256
<Dropdown.Menu>
53-
<Dropdown.Item disabled>
54-
{intl.formatMessage(messages.menuEdit)}
55-
</Dropdown.Item>
57+
{
58+
blockType === 'html' ? (
59+
<Dropdown.Item as={Link} to={`/library/${libraryId}/editor/${blockType}/${usageKey}`}>
60+
<FormattedMessage {...messages.menuEdit} />
61+
</Dropdown.Item>
62+
) : (
63+
<Dropdown.Item disabled>
64+
<FormattedMessage {...messages.menuEdit} />
65+
</Dropdown.Item>
66+
)
67+
}
5668
<Dropdown.Item onClick={updateClipboardClick}>
57-
{intl.formatMessage(messages.menuCopyToClipboard)}
69+
<FormattedMessage {...messages.menuCopyToClipboard} />
5870
</Dropdown.Item>
5971
<Dropdown.Item disabled>
60-
{intl.formatMessage(messages.menuAddToCollection)}
72+
<FormattedMessage {...messages.menuAddToCollection} />
6173
</Dropdown.Item>
6274
</Dropdown.Menu>
6375
</Dropdown>

src/library-authoring/data/apiHooks.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { camelCaseObject } from '@edx/frontend-platform';
22
import {
3-
useQuery, useMutation, useQueryClient, type Query,
3+
useQuery,
4+
useMutation,
5+
useQueryClient,
6+
type Query,
7+
type QueryClient,
48
} from '@tanstack/react-query';
59

610
import {
@@ -61,6 +65,22 @@ export const libraryAuthoringQueryKeys = {
6165
],
6266
};
6367

68+
/**
69+
* Tell react-query to refresh its cache of any data related to the given
70+
* component (XBlock).
71+
*
72+
* Note that technically it's possible to derive the library key from the
73+
* usageKey, so we could refactor this to only require the usageKey.
74+
*
75+
* @param queryClient The query client - get it via useQueryClient()
76+
* @param contentLibraryId The ID of library that holds the XBlock ("lib:...")
77+
* @param usageKey The usage ID of the XBlock ("lb:...")
78+
*/
79+
export function invalidateComponentData(queryClient: QueryClient, contentLibraryId: string, usageKey: string) {
80+
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey) });
81+
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
82+
}
83+
6484
/**
6585
* Hook to fetch a content library by its ID.
6686
*/
@@ -204,8 +224,7 @@ export const useUpdateXBlockFields = (contentLibraryId: string, usageKey: string
204224
);
205225
},
206226
onSettled: () => {
207-
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey) });
208-
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
227+
invalidateComponentData(queryClient, contentLibraryId, usageKey);
209228
},
210229
});
211230
};

0 commit comments

Comments
 (0)