Skip to content

Commit

Permalink
feat: In libraries, allow opening the editor for text (html) components
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Aug 29, 2024
1 parent dde8385 commit ac85a40
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/CourseAuthoringRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const CourseAuthoringRoutes = () => {
/>
<Route
path="editor/:blockType/:blockId?"
element={<PageWrap><EditorContainer courseId={courseId} /></PageWrap>}
element={<PageWrap><EditorContainer learningContextId={courseId} /></PageWrap>}
/>
<Route
path="settings/details"
Expand Down
28 changes: 0 additions & 28 deletions src/editors/EditorContainer.jsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/editors/EditorContainer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jest.mock('react-router', () => ({
}),
}));

const props = { courseId: 'cOuRsEId' };
const props = { learningContextId: 'cOuRsEId' };

describe('Editor Container', () => {
describe('snapshots', () => {
Expand Down
42 changes: 42 additions & 0 deletions src/editors/EditorContainer.tsx
Original file line number Diff line number Diff line change
@@ -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<string, any>) => void;
}

const EditorContainer: React.FC<Props> = ({
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 <div>Error: missing URL parameters</div>;
}
return (
<div className="editor-page">
<EditorPage
courseId={learningContextId}
blockType={blockType}
blockId={blockId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={onClose}
returnFunction={onSave}
/>
</div>
);
};

export default EditorContainer;
57 changes: 52 additions & 5 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<LibraryProvider>
<LibraryAuthoringPage />
</LibraryProvider>
);
return (
<LibraryProvider>
<Routes>
<Route
path="editor/:blockType/:blockId?"
element={(
<PageWrap>
<EditorContainer learningContextId={libraryId} onClose={goBack} onSave={goBack} />
</PageWrap>
)}
/>
<Route
path="*"
element={<LibraryAuthoringPage />}
/>
</Routes>
</LibraryProvider>
);
};

export default LibraryLayout;
24 changes: 18 additions & 6 deletions src/library-authoring/components/ComponentCard.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -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 = () => {
Expand All @@ -50,14 +54,22 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => {
data-testid="component-card-menu-toggle"
/>
<Dropdown.Menu>
<Dropdown.Item disabled>
{intl.formatMessage(messages.menuEdit)}
</Dropdown.Item>
{
blockType === 'html' ? (
<Dropdown.Item as={Link} to={`/library/${libraryId}/editor/${blockType}/${usageKey}`}>
<FormattedMessage {...messages.menuEdit} />
</Dropdown.Item>
) : (
<Dropdown.Item disabled>
<FormattedMessage {...messages.menuEdit} />
</Dropdown.Item>
)
}
<Dropdown.Item onClick={updateClipboardClick}>
{intl.formatMessage(messages.menuCopyToClipboard)}
<FormattedMessage {...messages.menuCopyToClipboard} />
</Dropdown.Item>
<Dropdown.Item disabled>
{intl.formatMessage(messages.menuAddToCollection)}
<FormattedMessage {...messages.menuAddToCollection} />
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
Expand Down
25 changes: 22 additions & 3 deletions src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
},
});
};

0 comments on commit ac85a40

Please sign in to comment.