feat: edit components in unit page [FC-0083] (#1821)

Allows authors to edit components from unit page. It makes sure that the component preview is updated on save, allows user to double click and open editor in modal etc.
This commit is contained in:
Navin Karkera
2025-04-17 14:59:16 +00:00
committed by GitHub
parent 3b2adc2fc1
commit 4ddb8c3168
8 changed files with 48 additions and 20 deletions

View File

@@ -13,6 +13,7 @@ export async function mockContentTaxonomyTagsData(contentId: string): Promise<an
case thisMock.languageWithTagsId: return thisMock.languageWithTags;
case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags;
case thisMock.largeTagsId: return thisMock.largeTags;
case thisMock.containerTagsId: return thisMock.largeTags;
case thisMock.emptyTagsId: return thisMock.emptyTags;
default: throw new Error(`No mock has been set up for contentId "${contentId}"`);
}
@@ -204,6 +205,7 @@ mockContentTaxonomyTagsData.emptyTagsId = 'block-v1:EmptyTagsOrg+STC1+2023_1+typ
mockContentTaxonomyTagsData.emptyTags = {
taxonomies: [],
};
mockContentTaxonomyTagsData.containerTagsId = 'lct:org:lib:unit:container_tags';
mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData);
/**

View File

@@ -32,6 +32,7 @@ const mockAddItemsToCollection = jest.fn();
const mockAddComponentsToContainer = jest.fn();
jest.spyOn(api, 'addItemsToCollection').mockImplementation(mockAddItemsToCollection);
jest.spyOn(api, 'addComponentsToContainer').mockImplementation(mockAddComponentsToContainer);
const unitId = 'lct:Axim:TEST:unit:test-unit-1';
const render = (context: 'collection' | 'unit') => baseRender(<PickLibraryContentModal isOpen onClose={onClose} />, {
path: context === 'collection'
@@ -40,7 +41,7 @@ const render = (context: 'collection' | 'unit') => baseRender(<PickLibraryConten
params: {
libraryId,
...(context === 'collection' && { collectionId: 'collectionId' }),
...(context === 'unit' && { unitId: 'unitId' }),
...(context === 'unit' && { unitId }),
},
extraWrapper: ({ children }) => (
<LibraryProvider
@@ -85,7 +86,7 @@ describe('<PickLibraryContentModal />', () => {
);
} else {
expect(mockAddComponentsToContainer).toHaveBeenCalledWith(
'unitId',
unitId,
['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'],
);
}
@@ -123,7 +124,7 @@ describe('<PickLibraryContentModal />', () => {
);
} else {
expect(mockAddComponentsToContainer).toHaveBeenCalledWith(
'unitId',
unitId,
['lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd'],
);
}

View File

@@ -482,7 +482,7 @@ export async function mockGetContainerMetadata(containerId: string): Promise<api
mockGetContainerMetadata.containerId = 'lct:org:lib:unit:test-unit-9a207';
mockGetContainerMetadata.containerIdError = 'lct:org:lib:unit:container_error';
mockGetContainerMetadata.containerIdLoading = 'lct:org:lib:unit:container_loading';
mockGetContainerMetadata.containerIdForTags = mockContentTaxonomyTagsData.largeTagsId;
mockGetContainerMetadata.containerIdForTags = mockContentTaxonomyTagsData.containerTagsId;
mockGetContainerMetadata.containerIdWithCollections = 'lct:org:lib:unit:container_collections';
mockGetContainerMetadata.containerData = {
id: 'lct:org:lib:unit:test-unit-9a2072',

View File

@@ -118,7 +118,7 @@ describe('library data API', () => {
it('should add components to unit', async () => {
const { axiosMock } = initializeMocks();
const componentId = 'lb:org:lib:html:1';
const containerId = 'ltc:org:lib:unit:1';
const containerId = 'lct:org:lib:unit:1';
const url = api.getLibraryContainerChildrenApiUrl(containerId);
axiosMock.onPost(url).reply(200);

View File

@@ -257,7 +257,7 @@ describe('library api hooks', () => {
it('should add components to container', async () => {
const componentId = 'lb:org:lib:html:1';
const containerId = 'ltc:org:lib:unit:1';
const containerId = 'lct:org:lib:unit:1';
const url = getLibraryContainerChildrenApiUrl(containerId);

View File

@@ -101,17 +101,27 @@ export const libraryAuthoringQueryKeys = {
'blockTypes',
libraryId,
],
container: (containerId?: string) => [
...libraryAuthoringQueryKeys.all,
'container',
containerId,
],
containerChildren: (containerId?: string) => [
...libraryAuthoringQueryKeys.all,
'container',
containerId,
'children',
],
container: (containerId?: string) => {
const baseKey = containerId
? libraryAuthoringQueryKeys.contentLibrary(getLibraryId(containerId))
: libraryAuthoringQueryKeys.all;
return [
...baseKey,
'container',
containerId,
];
},
containerChildren: (containerId?: string) => {
const baseKey = containerId
? libraryAuthoringQueryKeys.contentLibrary(getLibraryId(containerId))
: libraryAuthoringQueryKeys.all;
return [
...baseKey,
'container',
containerId,
'children',
];
},
};
export const xblockQueryKeys = {

View File

@@ -26,6 +26,7 @@ import { useLibraryRoutes } from '../routes';
import messages from './messages';
import { useSidebarContext } from '../common/context/SidebarContext';
import { ToastContext } from '../../generic/toast-context';
import { canEditComponent } from '../components/ComponentEditorModal';
/** Components that need large min height in preview */
const LARGE_COMPONENTS = [
@@ -90,6 +91,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
componentId,
readOnly,
setComponentId,
openComponentEditor,
} = useLibraryContext();
const {
@@ -131,9 +133,14 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
closeManageTagsDrawer();
};
const handleComponentSelection = (block: LibraryBlockMetadata) => {
const handleComponentSelection = (block: LibraryBlockMetadata, numberOfClicks: number) => {
setComponentId(block.id);
navigateTo({ componentId: block.id });
const canEdit = canEditComponent(block.id);
if (numberOfClicks > 1 && canEdit) {
// Open editor on double click.
openComponentEditor(block.id);
}
};
/* istanbul ignore next */
@@ -166,7 +173,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
};
const renderedBlocks = orderedBlocks?.map((block) => (
<IframeProvider key={block.id}>
<IframeProvider key={block.id + block.modified}>
<SortableItem
id={block.id}
componentStyle={null}
@@ -179,7 +186,7 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => {
outline: hidePreviewFor === block.id && '2px dashed gray',
}}
isClickable
onClick={() => handleComponentSelection(block)}
onClick={(e: { detail: number; }) => handleComponentSelection(block, e.detail)}
disabled={preview}
>
{hidePreviewFor !== block.id && (

View File

@@ -231,4 +231,12 @@ describe('<LibraryUnitPage />', () => {
});
await waitFor(() => expect(mockShowToast).toHaveBeenLastCalledWith('Failed to update components order'));
});
it('should show editor on double click', async () => {
renderLibraryUnitPage();
const component = await screen.findByText('text block 0');
// trigger double click
userEvent.click(component, undefined, { clickCount: 2 });
expect(await screen.findByRole('dialog', { name: 'Editor Dialog' })).toBeInTheDocument();
});
});