[Teak] fix: published name in unit sidebar in container picker & Issues on Inplace Editor (#2140)

Backport of fix: show unit published name in sidebar on content picker [FC-0090] #2100 
Backport of fix: Issue on the Inplace editor [FC-0090] #2101
This commit is contained in:
Chris Chávez
2025-06-17 19:58:57 -05:00
committed by GitHub
parent 4ba8cde587
commit c9896a8fe5
10 changed files with 117 additions and 51 deletions

View File

@@ -110,4 +110,25 @@ describe('<InplaceTextEditor />', () => {
// Show original text
expect(screen.getByText('Test text')).toBeInTheDocument();
});
it('should disappear edit button while editing', async () => {
render(<InplaceTextEditor text="Test text" onSave={mockOnSave} />);
const title = screen.getByText('Test text');
expect(title).toBeInTheDocument();
const editButton = screen.getByRole('button', { name: /edit/i });
expect(editButton).toBeInTheDocument();
fireEvent.click(editButton);
const textBox = screen.getByRole('textbox');
expect(editButton).not.toBeInTheDocument();
fireEvent.change(textBox, { target: { value: 'New text' } });
fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 });
expect(textBox).not.toBeInTheDocument();
expect(mockOnSave).toHaveBeenCalledWith('New text');
expect(await screen.findByRole('button', { name: /edit/i })).toBeInTheDocument();
});
});

View File

@@ -92,17 +92,19 @@ export const InplaceTextEditor: React.FC<InplaceTextEditorProps> = ({
/>
)
: (
<Truncate className={textClassName}>
{text}
</Truncate>
<>
<Truncate className={textClassName}>
{text}
</Truncate>
<IconButton
src={Edit}
iconAs={Icon}
alt={intl.formatMessage(messages.editTextButtonAlt)}
onClick={handleEdit}
size="sm"
/>
</>
)}
<IconButton
src={Edit}
iconAs={Icon}
alt={intl.formatMessage(messages.editTextButtonAlt)}
onClick={handleEdit}
size="inline"
/>
</Stack>
);
};

View File

@@ -113,7 +113,7 @@ export const SubHeaderTitle = ({ title }: { title: ReactNode }) => {
const showReadOnlyBadge = readOnly && !componentPickerMode;
return (
<Stack direction="vertical">
<Stack direction="vertical" className="mt-1.5">
{title}
{showReadOnlyBadge && (
<div>

View File

@@ -494,7 +494,7 @@
],
"created": 1742221203.895054,
"modified": 1742221203.895054,
"usage_key": "lct:Axim:TEST:unit:test-unit-9284e2",
"usage_key": "lct:org:lib:unit:test-unit-9a207",
"block_type": "unit",
"context_key": "lib:Axim:TEST",
"org": "Axim",
@@ -512,12 +512,18 @@
],
"created": "1742221203.895054",
"modified": "1742221203.895054",
"usage_key": "lct:Axim:TEST:unit:test-unit-9284e2",
"usage_key": "lct:org:lib:unit:test-unit-9a207",
"block_type": "unit",
"context_key": "lib:Axim:TEST",
"org": "Axim",
"access_id": "15",
"num_children": "0"
"num_children": "0",
"published": {
"display_name": "Published Test Unit"
}
},
"published": {
"display_name": "Published Test Unit"
}
}
],

View File

@@ -14,6 +14,7 @@ import {
mockGetCollectionMetadata,
mockGetContentLibraryV2List,
mockLibraryBlockMetadata,
mockGetContainerMetadata,
} from '../data/api.mocks';
import { ComponentPicker } from './ComponentPicker';
@@ -40,6 +41,7 @@ mockContentSearchConfig.applyMock();
mockGetCollectionMetadata.applyMock();
mockGetContentLibraryV2List.applyMock();
mockLibraryBlockMetadata.applyMock();
mockGetContainerMetadata.applyMock();
let postMessageSpy: jest.SpyInstance;
@@ -99,6 +101,24 @@ describe('<ComponentPicker />', () => {
}, '*');
});
it('should open the unit sidebar', async () => {
render(<ComponentPicker />);
expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i));
// Wait for the content library to load
await screen.findByText(/Change Library/i);
expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
// Click on the unit card to open the sidebar
fireEvent.click((await screen.findByText('Published Test Unit')));
const sidebar = await screen.findByTestId('library-sidebar');
expect(sidebar).toBeInTheDocument();
await waitFor(() => expect(within(sidebar).getByText('Published Test Unit')).toBeInTheDocument());
});
it('should pick component inside a collection using the card', async () => {
render(<ComponentPicker />);

View File

@@ -0,0 +1,48 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { useContext } from 'react';
import { InplaceTextEditor } from '../../generic/inplace-text-editor';
import { ToastContext } from '../../generic/toast-context';
import { useLibraryContext } from '../common/context/LibraryContext';
import { useContainer, useUpdateContainer } from '../data/apiHooks';
import messages from './messages';
interface EditableTitleProps {
containerId: string;
textClassName?: string;
}
export const ContainerEditableTitle = ({ containerId, textClassName }: EditableTitleProps) => {
const intl = useIntl();
const { readOnly, showOnlyPublished } = useLibraryContext();
const { data: container } = useContainer(containerId);
const updateMutation = useUpdateContainer(containerId);
const { showToast } = useContext(ToastContext);
const handleSaveDisplayName = async (newDisplayName: string) => {
try {
await updateMutation.mutateAsync({
displayName: newDisplayName,
});
showToast(intl.formatMessage(messages.updateContainerSuccessMsg));
} catch (err) {
showToast(intl.formatMessage(messages.updateContainerErrorMsg));
}
};
// istanbul ignore if: this should never happen
if (!container) {
return null;
}
return (
<InplaceTextEditor
onSave={handleSaveDisplayName}
text={showOnlyPublished ? (container.publishedDisplayName ?? container.displayName) : container.displayName}
readOnly={readOnly}
textClassName={textClassName}
/>
);
};

View File

@@ -1,17 +1,7 @@
import { useContext } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { InplaceTextEditor } from '../../generic/inplace-text-editor';
import { ToastContext } from '../../generic/toast-context';
import { useLibraryContext } from '../common/context/LibraryContext';
import { useSidebarContext } from '../common/context/SidebarContext';
import { useContainer, useUpdateContainer } from '../data/apiHooks';
import messages from './messages';
import { ContainerEditableTitle } from './ContainerEditableTitle';
const ContainerInfoHeader = () => {
const intl = useIntl();
const { readOnly } = useLibraryContext();
const { sidebarComponentInfo } = useSidebarContext();
const containerId = sidebarComponentInfo?.id;
@@ -20,32 +10,9 @@ const ContainerInfoHeader = () => {
throw new Error('containerId is required');
}
const { data: container } = useContainer(containerId);
const updateMutation = useUpdateContainer(containerId);
const { showToast } = useContext(ToastContext);
const handleSaveDisplayName = async (newDisplayName: string) => {
try {
await updateMutation.mutateAsync({
displayName: newDisplayName,
});
showToast(intl.formatMessage(messages.updateContainerSuccessMsg));
} catch (err) {
showToast(intl.formatMessage(messages.updateContainerErrorMsg));
throw err;
}
};
if (!container) {
return null;
}
return (
<InplaceTextEditor
onSave={handleSaveDisplayName}
text={container.displayName}
readOnly={readOnly}
<ContainerEditableTitle
containerId={containerId}
textClassName="font-weight-bold m-1.5"
/>
);

View File

@@ -495,6 +495,7 @@ mockGetContainerMetadata.containerData = {
id: 'lct:org:lib:unit:test-unit-9a2072',
containerType: 'unit',
displayName: 'Test Unit',
publishedDisplayName: 'Published Test Unit',
created: '2024-09-19T10:00:00Z',
createdBy: 'test_author',
lastPublished: '2024-09-20T10:00:00Z',

View File

@@ -600,6 +600,7 @@ export interface Container {
id: string;
containerType: 'unit';
displayName: string;
publishedDisplayName: string;
lastPublished: string | null;
publishedBy: string | null;
createdBy: string | null;

View File

@@ -615,7 +615,7 @@ export const useUpdateContainer = (containerId: string) => {
return useMutation({
mutationFn: (data: api.UpdateContainerDataRequest) => api.updateContainerMetadata(containerId, data),
onMutate: (data) => {
const previousData = queryClient.getQueryData(containerQueryKey) as api.CollectionMetadata;
const previousData = queryClient.getQueryData(containerQueryKey) as api.Container;
queryClient.setQueryData(containerQueryKey, {
...previousData,
...data,