+
- )
- : (
- <>
+
+ )}
+ >
+
+ {inputIsActive
+ ? (
+
+ )
+ : (
{text}
- {!readOnly && showEditButton && (
-
- )}
- >
- )}
-
+ )}
+
+
);
};
diff --git a/src/library-authoring/collections/CollectionInfoHeader.tsx b/src/library-authoring/collections/CollectionInfoHeader.tsx
index 1e31aadba..8f476d35e 100644
--- a/src/library-authoring/collections/CollectionInfoHeader.tsx
+++ b/src/library-authoring/collections/CollectionInfoHeader.tsx
@@ -46,7 +46,7 @@ const CollectionInfoHeader = () => {
text={collection.title}
readOnly={readOnly}
textClassName="font-weight-bold m-1.5"
- showEditButton
+ alwaysShowEditButton
/>
);
};
diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx
index 027ea60c0..62ad7573d 100644
--- a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx
+++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx
@@ -98,7 +98,7 @@ describe('', () => {
});
});
- it('should close edit library title on press Escape', async () => {
+ it('should close edit component title on press Escape', async () => {
const url = getXBlockFieldsVersionApiUrl(usageKey, 'draft');
axiosMock.onPost(url).reply(200);
render();
@@ -117,7 +117,7 @@ describe('', () => {
await waitFor(() => expect(axiosMock.history.post.length).toEqual(0));
});
- it('should show error on edit library tittle', async () => {
+ it('should show error on edit component tittle', async () => {
const url = getXBlockFieldsApiUrl(usageKey);
axiosMock.onPatch(url).reply(500);
render();
diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx
index 1cabfacd4..0757c9775 100644
--- a/src/library-authoring/component-info/ComponentInfoHeader.tsx
+++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx
@@ -48,7 +48,7 @@ const ComponentInfoHeader = () => {
text={xblockFields?.displayName}
readOnly={readOnly}
textClassName="font-weight-bold m-1.5"
- showEditButton
+ alwaysShowEditButton
/>
);
};
diff --git a/src/library-authoring/containers/ContainerInfoHeader.tsx b/src/library-authoring/containers/ContainerInfoHeader.tsx
index bb2b2c9d0..39d590db6 100644
--- a/src/library-authoring/containers/ContainerInfoHeader.tsx
+++ b/src/library-authoring/containers/ContainerInfoHeader.tsx
@@ -45,7 +45,7 @@ const ContainerInfoHeader = () => {
text={container.displayName}
readOnly={readOnly}
textClassName="font-weight-bold m-1.5"
- showEditButton
+ alwaysShowEditButton
/>
);
};
diff --git a/src/library-authoring/data/apiHooks.test.tsx b/src/library-authoring/data/apiHooks.test.tsx
index 83153de5a..47ee77693 100644
--- a/src/library-authoring/data/apiHooks.test.tsx
+++ b/src/library-authoring/data/apiHooks.test.tsx
@@ -164,7 +164,7 @@ describe('library api hooks', () => {
});
it('should delete a container', async () => {
- const containerId = 'lct:org:lib1';
+ const containerId = 'lct:org:lib:unit:unit1';
const url = getLibraryContainerApiUrl(containerId);
axiosMock.onDelete(url).reply(200);
@@ -176,7 +176,7 @@ describe('library api hooks', () => {
});
it('should restore a container', async () => {
- const containerId = 'lct:org:lib1';
+ const containerId = 'lct:org:lib:unit:unit1';
const url = getLibraryContainerRestoreApiUrl(containerId);
axiosMock.onPost(url).reply(200);
@@ -272,7 +272,7 @@ describe('library api hooks', () => {
});
it('should update container children', async () => {
- const containerId = 'lct:org:lib1';
+ const containerId = 'lct:org:lib:unit:unit-1';
const url = getLibraryContainerChildrenApiUrl(containerId);
axiosMock.onPatch(url).reply(200);
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 6de238c99..e9a5202b1 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -329,9 +329,13 @@ export const useUpdateXBlockFields = (usageKey: string) => {
mutationFn: (data: api.UpdateXBlockFieldsRequest) => api.updateXBlockFields(usageKey, data),
onMutate: async (data) => {
const queryKey = xblockQueryKeys.xblockFields(usageKey);
- const previousBlockData = queryClient.getQueriesData(queryKey)[0][1] as api.XBlockFields;
+ const previousBlockData = queryClient.getQueriesData(queryKey)?.[0]?.[1] as api.XBlockFields | undefined;
const formatedData = camelCaseObject(data);
+ if (!previousBlockData) {
+ return { previousBlockData };
+ }
+
const newBlockData = {
...previousBlockData,
...(formatedData.metadata?.displayName && { displayName: formatedData.metadata.displayName }),
@@ -343,7 +347,7 @@ export const useUpdateXBlockFields = (usageKey: string) => {
queryClient.setQueryData(queryKey, newBlockData);
- return { previousBlockData, newBlockData };
+ return { previousBlockData };
},
onError: (_err, _data, context) => {
queryClient.setQueryData(
diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx
index 87aacfa04..1ef20fa39 100644
--- a/src/library-authoring/units/LibraryUnitBlocks.tsx
+++ b/src/library-authoring/units/LibraryUnitBlocks.tsx
@@ -14,13 +14,19 @@ import ErrorAlert from '../../generic/alert-error';
import { getItemIcon } from '../../generic/block-type-utils';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
+import { InplaceTextEditor } from '../../generic/inplace-text-editor';
import Loading from '../../generic/Loading';
import TagCount from '../../generic/tag-count';
import { useLibraryContext } from '../common/context/LibraryContext';
import { PickLibraryContentModal } from '../add-content';
import ComponentMenu from '../components';
import { LibraryBlockMetadata } from '../data/api';
-import { libraryAuthoringQueryKeys, useContainerChildren, useUpdateContainerChildren } from '../data/apiHooks';
+import {
+ libraryAuthoringQueryKeys,
+ useContainerChildren,
+ useUpdateContainerChildren,
+ useUpdateXBlockFields,
+} from '../data/apiHooks';
import { LibraryBlock } from '../LibraryBlock';
import { useLibraryRoutes } from '../routes';
import messages from './messages';
@@ -43,30 +49,52 @@ interface BlockHeaderProps {
}
/** Component header, split out to reuse in drag overlay */
-const BlockHeader = ({ block, onTagClick }: BlockHeaderProps) => (
- <>
-
-
- {block.displayName}
-
-
-
- {block.hasUnpublishedChanges && (
-
-
-
-
-
-
- )}
-
-
-
- >
-);
+const BlockHeader = ({ block, onTagClick }: BlockHeaderProps) => {
+ const intl = useIntl();
+ const { showToast } = useContext(ToastContext);
+
+ const updateMutation = useUpdateXBlockFields(block.id);
+
+ const handleSaveDisplayName = (newDisplayName: string) => {
+ updateMutation.mutateAsync({
+ metadata: {
+ display_name: newDisplayName,
+ },
+ }).then(() => {
+ showToast(intl.formatMessage(messages.updateComponentSuccessMsg));
+ }).catch(() => {
+ showToast(intl.formatMessage(messages.updateComponentErrorMsg));
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {block.hasUnpublishedChanges && (
+
+
+
+
+
+
+ )}
+
+
+
+ >
+ );
+};
interface LibraryUnitBlocksProps {
/** set to true if it is rendered as preview
diff --git a/src/library-authoring/units/LibraryUnitPage.test.tsx b/src/library-authoring/units/LibraryUnitPage.test.tsx
index 03ac5b566..f90dcedd1 100644
--- a/src/library-authoring/units/LibraryUnitPage.test.tsx
+++ b/src/library-authoring/units/LibraryUnitPage.test.tsx
@@ -10,7 +10,11 @@ import {
waitFor,
within,
} from '../../testUtils';
-import { getLibraryContainerApiUrl, getLibraryContainerChildrenApiUrl } from '../data/api';
+import {
+ getLibraryContainerApiUrl,
+ getLibraryContainerChildrenApiUrl,
+ getXBlockFieldsApiUrl,
+} from '../data/api';
import {
mockContentLibrary,
mockXBlockFields,
@@ -198,6 +202,74 @@ describe('', () => {
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
+ it('should rename component while clicking on name', async () => {
+ const url = getXBlockFieldsApiUrl('lb:org1:Demo_course:html:text-0');
+ axiosMock.onPost(url).reply(200);
+ renderLibraryUnitPage();
+
+ // Wait loading of the component
+ await screen.findByText('text block 0');
+
+ const componentTitle = screen.getAllByRole(
+ 'button',
+ { name: 'text block 0' },
+ )[0];
+ fireEvent.click(componentTitle);
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name: /text input/i })).toBeInTheDocument();
+ });
+
+ const textBox = screen.getByRole('textbox', { name: /text input/i });
+ expect(textBox).toBeInTheDocument();
+ fireEvent.change(textBox, { target: { value: 'New Component Title' } });
+ fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
+
+ await waitFor(() => {
+ expect(axiosMock.history.post.length).toEqual(1);
+ });
+ expect(axiosMock.history.post[0].url).toEqual(url);
+ expect(axiosMock.history.post[0].data).toStrictEqual(JSON.stringify({
+ metadata: { display_name: 'New Component Title' },
+ }));
+ expect(textBox).not.toBeInTheDocument();
+ expect(mockShowToast).toHaveBeenCalledWith('Component updated successfully.');
+ });
+
+ it('should show error while updating component name', async () => {
+ const url = getXBlockFieldsApiUrl('lb:org1:Demo_course:html:text-0');
+ axiosMock.onPost(url).reply(400);
+ renderLibraryUnitPage();
+
+ // Wait loading of the component
+ await screen.findByText('text block 0');
+
+ const componentTitle = screen.getAllByRole(
+ 'button',
+ { name: 'text block 0' },
+ )[0];
+ fireEvent.click(componentTitle);
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name: /text input/i })).toBeInTheDocument();
+ });
+
+ const textBox = screen.getByRole('textbox', { name: /text input/i });
+ expect(textBox).toBeInTheDocument();
+ fireEvent.change(textBox, { target: { value: 'New Component Title' } });
+ fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
+
+ await waitFor(() => {
+ expect(axiosMock.history.post.length).toEqual(1);
+ });
+ expect(axiosMock.history.post[0].url).toEqual(url);
+ expect(axiosMock.history.post[0].data).toStrictEqual(JSON.stringify({
+ metadata: { display_name: 'New Component Title' },
+ }));
+ expect(textBox).not.toBeInTheDocument();
+ expect(mockShowToast).toHaveBeenCalledWith('There was an error updating the component.');
+ });
+
it('should call update order api on dragging component', async () => {
renderLibraryUnitPage();
const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0];
diff --git a/src/library-authoring/units/messages.ts b/src/library-authoring/units/messages.ts
index a15eeb850..3b9a8fdc3 100644
--- a/src/library-authoring/units/messages.ts
+++ b/src/library-authoring/units/messages.ts
@@ -31,6 +31,16 @@ const messages = defineMessages({
defaultMessage: 'Draft',
description: 'Chip in components in unit page that is shown when component has unpublished changes',
},
+ updateComponentSuccessMsg: {
+ id: 'course-authoring.library-authoring.unit-component.update.success',
+ defaultMessage: 'Component updated successfully.',
+ description: 'Message when the component is updated successfully',
+ },
+ updateComponentErrorMsg: {
+ id: 'course-authoring.library-authoring.unit-component.update.error',
+ defaultMessage: 'There was an error updating the component.',
+ description: 'Message when there is an error when updating the component',
+ },
updateContainerSuccessMsg: {
id: 'course-authoring.library-authoring.update-container-success-msg',
defaultMessage: 'Container updated successfully.',