Skip to content

Commit

Permalink
feat: OLX editing
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Sep 28, 2024
1 parent b4a1861 commit bc07fcb
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 10 deletions.
55 changes: 55 additions & 0 deletions src/generic/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable import/prefer-default-export */
import React from 'react';
import { basicSetup, EditorView } from 'codemirror';
import { EditorState, Compartment } from '@codemirror/state';
import { xml } from '@codemirror/lang-xml';

export type EditorAccessor = EditorView;

interface Props {
readOnly?: boolean;
children?: string;
editorRef?: React.MutableRefObject<EditorAccessor | undefined>;
}

export const CodeEditor: React.FC<Props> = ({
readOnly = false,
children = '',
editorRef,
}) => {
const divRef = React.useRef<HTMLDivElement>(null);
const language = React.useMemo(() => new Compartment(), []);
const tabSize = React.useMemo(() => new Compartment(), []);

Check warning on line 22 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L19-L22

Added lines #L19 - L22 were not covered by tests

React.useEffect(() => {

Check warning on line 24 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L24

Added line #L24 was not covered by tests
if (!divRef.current) { return; }
const state = EditorState.create({

Check warning on line 26 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L26

Added line #L26 was not covered by tests
doc: children,
extensions: [
basicSetup,
language.of(xml()),
tabSize.of(EditorState.tabSize.of(2)),
EditorState.readOnly.of(readOnly),
],
});

const view = new EditorView({

Check warning on line 36 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L36

Added line #L36 was not covered by tests
state,
parent: divRef.current,
});
if (editorRef) {
// eslint-disable-next-line no-param-reassign
editorRef.current = view;

Check warning on line 42 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L42

Added line #L42 was not covered by tests
}
// eslint-disable-next-line consistent-return
return () => {

Check warning on line 45 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L45

Added line #L45 was not covered by tests
if (editorRef) {
// eslint-disable-next-line no-param-reassign
editorRef.current = undefined;

Check warning on line 48 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L48

Added line #L48 was not covered by tests
}
view.destroy(); // Clean up

Check warning on line 50 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L50

Added line #L50 was not covered by tests
};
}, [divRef.current, readOnly, editorRef]);

return <div ref={divRef} />;

Check warning on line 54 in src/generic/CodeEditor.tsx

View check run for this annotation

Codecov / codecov/patch

src/generic/CodeEditor.tsx#L54

Added line #L54 was not covered by tests
};
68 changes: 59 additions & 9 deletions src/library-authoring/component-info/ComponentAdvancedInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/* eslint-disable import/prefer-default-export */
import React from 'react';
import { Collapsible } from '@openedx/paragon';
import {
Button,
Collapsible,
OverlayTrigger,
Tooltip,
} from '@openedx/paragon';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';

import { LoadingSpinner } from '../../generic/Loading';
import { useXBlockOLX } from '../data/apiHooks';
import { CodeEditor, EditorAccessor } from '../../generic/CodeEditor';
import { useUpdateXBlockOLX, useXBlockOLX } from '../data/apiHooks';
import messages from './messages';

interface Props {
Expand All @@ -13,7 +19,27 @@ interface Props {

export const ComponentAdvancedInfo: React.FC<Props> = ({ usageKey }) => {
const intl = useIntl();
// TODO: hide the "Edit" button if the library is read only
const { data: olx, isLoading: isOLXLoading } = useXBlockOLX(usageKey);
const editorRef = React.useRef<EditorAccessor | undefined>(undefined);
const [isEditingOLX, setEditingOLX] = React.useState(false);
const olxUpdater = useUpdateXBlockOLX(usageKey);
const updateOlx = React.useCallback(() => {
const newOLX = editorRef.current?.state.doc.toString();
if (!newOLX) {
/* istanbul ignore next */
throw new Error('Unable to get OLX string from codemirror.'); // Shouldn't happen.
}
olxUpdater.mutateAsync(newOLX).catch(err => {

Check warning on line 33 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L33

Added line #L33 was not covered by tests
// eslint-disable-next-line no-console
console.error(err);

Check warning on line 35 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L35

Added line #L35 was not covered by tests
// eslint-disable-next-line no-alert
alert(intl.formatMessage(messages.advancedDetailsOLXEditFailed));
}).then(() => {

Check warning on line 38 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L37-L38

Added lines #L37 - L38 were not covered by tests
// Only if we succeeded:
setEditingOLX(false);

Check warning on line 40 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L40

Added line #L40 was not covered by tests
});
}, [editorRef, olxUpdater, intl]);
return (
<Collapsible
styling="basic"
Expand All @@ -22,13 +48,37 @@ export const ComponentAdvancedInfo: React.FC<Props> = ({ usageKey }) => {
<dl>
<dt><FormattedMessage {...messages.advancedDetailsUsageKey} /></dt>
<dd className="text-monospace small">{usageKey}</dd>
<dt>OLX Source</dt>
<dd>
{
olx ? <code className="micro">{olx}</code> : // eslint-disable-line
isOLXLoading ? <LoadingSpinner /> : // eslint-disable-line
<span>Error</span>
}
<dt><FormattedMessage {...messages.advancedDetailsOLX} /></dt>
<dd>{(() => {
if (isOLXLoading) { return <LoadingSpinner />; }
if (!olx) { return <FormattedMessage {...messages.advancedDetailsOLXError} />; }
return (

Check warning on line 55 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L55

Added line #L55 was not covered by tests
<>
<CodeEditor readOnly={!isEditingOLX} editorRef={editorRef}>{olx}</CodeEditor>
{
isEditingOLX
? (
<>

Check warning on line 61 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L61

Added line #L61 was not covered by tests
<Button variant="primary" onClick={updateOlx} disabled={olxUpdater.isLoading}>Save</Button>
<Button variant="link" onClick={() => setEditingOLX(false)} disabled={olxUpdater.isLoading}>Cancel</Button>

Check warning on line 63 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L63

Added line #L63 was not covered by tests
</>
)
: (
<OverlayTrigger

Check warning on line 67 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L67

Added line #L67 was not covered by tests
placement="bottom-start"
overlay={(
<Tooltip id="olx-edit-button">
<FormattedMessage {...messages.advancedDetailsOLXEditWarning} />
</Tooltip>
)}
>
<Button variant="link" onClick={() => setEditingOLX(true)}><FormattedMessage {...messages.advancedDetailsOLXEditButton} /></Button>

Check warning on line 75 in src/library-authoring/component-info/ComponentAdvancedInfo.tsx

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/component-info/ComponentAdvancedInfo.tsx#L75

Added line #L75 was not covered by tests
</OverlayTrigger>
)
}
</>
);
})()}
</dd>
</dl>
</Collapsible>
Expand Down
20 changes: 20 additions & 0 deletions src/library-authoring/component-info/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ const messages = defineMessages({
defaultMessage: 'OLX Source',
description: 'Heading for the component\'s OLX source code',
},
advancedDetailsOLXEditButton: {
id: 'course-authoring.library-authoring.component.advanced.olx-edit',
defaultMessage: 'Edit OLX',
description: 'Heading for the component\'s OLX source code',
},
advancedDetailsOLXEditWarning: {
id: 'course-authoring.library-authoring.component.advanced.olx-warning',
defaultMessage: 'Be careful! This is an advanced feature and errors may break the component.',
description: 'Warning for users about editing OLX directly.',
},
advancedDetailsOLXEditFailed: {
id: 'course-authoring.library-authoring.component.advanced.olx-failed',
defaultMessage: 'An error occurred and the OLX could not be saved.',
description: 'Error message shown when saving the OLX fails.',
},
advancedDetailsOLXError: {
id: 'course-authoring.library-authoring.component.advanced.olx-error',
defaultMessage: 'Unable to load OLX',
description: 'Error message if OLX is unavailable',
},
advancedDetailsUsageKey: {
id: 'course-authoring.library-authoring.component.advanced.usage-key',
defaultMessage: 'ID (Usage key)',
Expand Down
9 changes: 9 additions & 0 deletions src/library-authoring/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,15 @@ export async function getXBlockOLX(usageKey: string): Promise<string> {
return data.olx;
}

/**
* Set the OLX for the given XBlock.
* Returns the OLX as it was actually saved.
*/
export async function setXBlockOLX(usageKey: string, newOLX: string): Promise<string> {
const { data } = await getAuthenticatedHttpClient().post(getXBlockOLXApiUrl(usageKey), { olx: newOLX });
return data.olx;

Check warning on line 327 in src/library-authoring/data/api.ts

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/data/api.ts#L326-L327

Added lines #L326 - L327 were not covered by tests
}

/**
* Get the collection metadata.
*/
Expand Down
22 changes: 21 additions & 1 deletion src/library-authoring/data/apiHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
updateCollectionComponents,
type CreateLibraryCollectionDataRequest,
getCollectionMetadata,
setXBlockOLX,
} from './api';

export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => {
Expand Down Expand Up @@ -272,7 +273,7 @@ export const useCreateLibraryCollection = (libraryId: string) => {
});
};

/* istanbul ignore next */ // This is only used in developer builds, and the associated UI doesn't work in test or prod
/** Get the OLX source of a library component */
export const useXBlockOLX = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.xblockOLX(usageKey),
Expand All @@ -281,6 +282,25 @@ export const useXBlockOLX = (usageKey: string) => (
})
);

/**
* Update the OLX of a library component (advanced feature)
*/
export const useUpdateXBlockOLX = (usageKey: string) => {
const contentLibraryId = getLibraryId(usageKey);
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newOLX: string) => setXBlockOLX(usageKey, newOLX),
onSuccess: (olxFromServer) => {
queryClient.setQueryData(xblockQueryKeys.xblockOLX(usageKey), olxFromServer);

Check warning on line 294 in src/library-authoring/data/apiHooks.ts

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/data/apiHooks.ts#L292-L294

Added lines #L292 - L294 were not covered by tests
// Reload the other data for this component:
invalidateComponentData(queryClient, contentLibraryId, usageKey);

Check warning on line 296 in src/library-authoring/data/apiHooks.ts

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/data/apiHooks.ts#L296

Added line #L296 was not covered by tests
// And the description and display name etc. may have changed, so refresh everything in the library too:
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(contentLibraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });

Check warning on line 299 in src/library-authoring/data/apiHooks.ts

View check run for this annotation

Codecov / codecov/patch

src/library-authoring/data/apiHooks.ts#L298-L299

Added lines #L298 - L299 were not covered by tests
},
});
};

/**
* Get the metadata for a collection in a library
*/
Expand Down

0 comments on commit bc07fcb

Please sign in to comment.