From 867d3a8f05dd88776c0250d2c9090792bd6751e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 16 Aug 2024 16:24:01 -0300 Subject: [PATCH 01/13] feat: add library component sidebar --- .../LibraryAuthoringPage.test.tsx | 101 +++++----- src/library-authoring/common/context.tsx | 31 ++- .../component-info/ComponentInfo.tsx | 51 +++++ .../ComponentInfoHeader.test.tsx | 178 ++++++++++++++++++ .../component-info/ComponentInfoHeader.tsx | 98 ++++++++++ .../component-info/index.tsx | 2 + .../component-info/messages.ts | 50 +++++ .../components/ComponentCard.tsx | 16 +- src/library-authoring/components/index.ts | 2 +- src/library-authoring/data/api.ts | 33 ++++ src/library-authoring/data/apiHooks.ts | 59 +++++- .../library-info/LibraryInfoHeader.tsx | 2 +- .../library-sidebar/LibrarySidebar.tsx | 23 ++- 13 files changed, 584 insertions(+), 62 deletions(-) create mode 100644 src/library-authoring/component-info/ComponentInfo.tsx create mode 100644 src/library-authoring/component-info/ComponentInfoHeader.test.tsx create mode 100644 src/library-authoring/component-info/ComponentInfoHeader.tsx create mode 100644 src/library-authoring/component-info/index.tsx create mode 100644 src/library-authoring/component-info/messages.ts diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 3dbd737bc5..24c4a7f926 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -10,13 +10,14 @@ import { render, waitFor, screen, + within, } from '@testing-library/react'; import fetchMock from 'fetch-mock-jest'; import initializeStore from '../store'; import { getContentSearchConfigUrl } from '../search-manager/data/api'; import mockResult from '../search-modal/__mocks__/search-result.json'; import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; -import { getContentLibraryApiUrl, type ContentLibrary } from './data/api'; +import { getContentLibraryApiUrl, getXBlockFieldsApiUrl, type ContentLibrary } from './data/api'; import { LibraryLayout } from '.'; let store; @@ -61,16 +62,17 @@ const returnEmptyResult = (_url, req) => { const returnLowNumberResults = (_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const query = requestData?.queries[0]?.q ?? ''; + const newMockResult = { ...mockResult }; // We have to replace the query (search keywords) in the mock results with the actual query, // because otherwise we may have an inconsistent state that causes more queries and unexpected results. - mockResult.results[0].query = query; + newMockResult.results[0].query = query; // Limit number of results to just 2 - mockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2); - mockResult.results[0].estimatedTotalHits = 2; + newMockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2); + newMockResult.results[0].estimatedTotalHits = 2; // And fake the required '_formatted' fields; it contains the highlighting ... around matched words // eslint-disable-next-line no-underscore-dangle, no-param-reassign - mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); - return mockResult; + newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + return newMockResult; }; const libraryData: ContentLibrary = { @@ -97,6 +99,13 @@ const libraryData: ContentLibrary = { updated: '2024-07-20', }; +const xBlockFields = { + display_name: 'Test HTML Block', + metadata: { + display_name: 'Test HTML Block', + }, +}; + const clipboardBroadcastChannelMock = { postMessage: jest.fn(), close: jest.fn(), @@ -158,6 +167,20 @@ describe('', () => { queryClient.clear(); }); + const renderLibraryPage = async () => { + mockUseParams.mockReturnValue({ libraryId: libraryData.id }); + axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); + + const result = render(); + + // Ensure the search endpoint is called: + // Call 1: To fetch searchable/filterable/sortable library data + // Call 2: To fetch the recently modified components only + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + + return result; + }; + it('shows the spinner before the query is complete', () => { mockUseParams.mockReturnValue({ libraryId: '1' }); // @ts-ignore Use unresolved promise to keep the Loading visible @@ -185,17 +208,9 @@ describe('', () => { }); it('show library data', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - const { getByRole, getByText, queryByText, findByText, findAllByText, - } = render(); - - // Ensure the search endpoint is called: - // Call 1: To fetch searchable/filterable/sortable library data - // Call 2: To fetch the recently modified components only - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + } = await renderLibraryPage(); expect(await findByText('Content library')).toBeInTheDocument(); expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument(); @@ -265,10 +280,7 @@ describe('', () => { }); it('show new content button', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - - render(); + await renderLibraryPage(); expect(await screen.findByRole('heading')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument(); @@ -327,10 +339,7 @@ describe('', () => { }); it('should open and close new content sidebar', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - - render(); + await renderLibraryPage(); expect(await screen.findByRole('heading')).toBeInTheDocument(); expect(screen.queryByText(/add content/i)).not.toBeInTheDocument(); @@ -347,10 +356,7 @@ describe('', () => { }); it('should open Library Info by default', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - - render(); + await renderLibraryPage(); expect(await screen.findByText('Content library')).toBeInTheDocument(); expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument(); @@ -366,10 +372,7 @@ describe('', () => { }); it('should close and open Library Info', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - - render(); + await renderLibraryPage(); expect(await screen.findByText('Content library')).toBeInTheDocument(); expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument(); @@ -389,17 +392,9 @@ describe('', () => { }); it('show the "View All" button when viewing library with many components', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - const { getByRole, getByText, queryByText, getAllByText, findAllByText, - } = render(); - - // Ensure the search endpoint is called: - // Call 1: To fetch searchable/filterable/sortable library data - // Call 2: To fetch the recently modified components only - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + } = await renderLibraryPage(); expect(getByText('Content library')).toBeInTheDocument(); expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument(); @@ -458,13 +453,9 @@ describe('', () => { }); it('sort library components', async () => { - mockUseParams.mockReturnValue({ libraryId: libraryData.id }); - axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData); - fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); - const { findByTitle, getAllByText, getByText, getByTitle, - } = render(); + } = await renderLibraryPage(); expect(await findByTitle('Sort search results')).toBeInTheDocument(); @@ -504,4 +495,24 @@ describe('', () => { fireEvent.click(getAllByText('Clear Filters')[0]); await testSortOption('', ''); }); + + it('should open and close the component sidebar', async () => { + const usageKey = mockResult.results[0].hits[0].usage_key; + const { getAllByText, queryByTestId } = await renderLibraryPage(); + axiosMock.onGet(getXBlockFieldsApiUrl(usageKey)).reply(200, xBlockFields); + + // Click on the first component + fireEvent.click(getAllByText('Test HTML Block')[0]); + + const sidebar = screen.getByTestId('library-sidebar'); + + const { getByRole, getByText } = within(sidebar); + + await waitFor(() => expect(getByText('Test HTML Block')).toBeInTheDocument()); + + const closeButton = getByRole('button', { name: /close/i }); + fireEvent.click(closeButton); + + await waitFor(() => expect(queryByTestId('library-sidebar')).not.toBeInTheDocument()); + }); }); diff --git a/src/library-authoring/common/context.tsx b/src/library-authoring/common/context.tsx index 548b39aa35..bf7f98e982 100644 --- a/src/library-authoring/common/context.tsx +++ b/src/library-authoring/common/context.tsx @@ -4,6 +4,7 @@ import React from 'react'; export enum SidebarBodyComponentId { AddContent = 'add-content', Info = 'info', + ComponentInfo = 'component-info', } export interface LibraryContextData { @@ -11,6 +12,8 @@ export interface LibraryContextData { closeLibrarySidebar: () => void; openAddContentSidebar: () => void; openInfoSidebar: () => void; + openComponentInfoSidebar: (usageKey: string) => void; + currentComponentUsageKey?: string; } export const LibraryContext = React.createContext({ @@ -18,6 +21,7 @@ export const LibraryContext = React.createContext({ closeLibrarySidebar: () => {}, openAddContentSidebar: () => {}, openInfoSidebar: () => {}, + openComponentInfoSidebar: (_usageKey: string) => {}, // eslint-disable-line @typescript-eslint/no-unused-vars } as LibraryContextData); /** @@ -25,21 +29,42 @@ export const LibraryContext = React.createContext({ */ export const LibraryProvider = (props: { children?: React.ReactNode }) => { const [sidebarBodyComponent, setSidebarBodyComponent] = React.useState(null); + const [currentComponentUsageKey, setCurrentComponentUsageKey] = React.useState(); - const closeLibrarySidebar = React.useCallback(() => setSidebarBodyComponent(null), []); - const openAddContentSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.AddContent), []); - const openInfoSidebar = React.useCallback(() => setSidebarBodyComponent(SidebarBodyComponentId.Info), []); + const closeLibrarySidebar = React.useCallback(() => { + setSidebarBodyComponent(null); + setCurrentComponentUsageKey(undefined); + }, []); + const openAddContentSidebar = React.useCallback(() => { + setCurrentComponentUsageKey(undefined); + setSidebarBodyComponent(SidebarBodyComponentId.AddContent); + }, []); + const openInfoSidebar = React.useCallback(() => { + setCurrentComponentUsageKey(undefined); + setSidebarBodyComponent(SidebarBodyComponentId.Info); + }, []); + const openComponentInfoSidebar = React.useCallback( + (usageKey: string) => { + setCurrentComponentUsageKey(usageKey); + setSidebarBodyComponent(SidebarBodyComponentId.ComponentInfo); + }, + [], + ); const context = React.useMemo(() => ({ sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar, openInfoSidebar, + openComponentInfoSidebar, + currentComponentUsageKey, }), [ sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar, openInfoSidebar, + openComponentInfoSidebar, + currentComponentUsageKey, ]); return ( diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx new file mode 100644 index 0000000000..62bcc0387d --- /dev/null +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -0,0 +1,51 @@ +/* eslint-disable react/require-default-props */ +import React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Button, + Tab, + Tabs, + Stack, +} from '@openedx/paragon'; + +import { ComponentMenu } from '../components'; +import messages from './messages'; + +interface ComponentInfoProps { + usageKey: string; +} + +const ComponentInfo = ({ usageKey } : ComponentInfoProps) => { + const intl = useIntl(); + + return ( + + + + + + + + + Preview tab placeholder + + + Manage tab placeholder + + + Details tab placeholder + + + + ); +}; + +export default ComponentInfo; diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx new file mode 100644 index 0000000000..ea7d794b83 --- /dev/null +++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx @@ -0,0 +1,178 @@ +/* eslint-disable react/require-default-props */ +import React from 'react'; +import MockAdapter from 'axios-mock-adapter'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + render, + fireEvent, + waitFor, +} from '@testing-library/react'; +import { ContentLibrary, getXBlockFieldsApiUrl } from '../data/api'; +import initializeStore from '../../store'; +import { ToastProvider } from '../../generic/toast-context'; +import ComponentInfoHeader from './ComponentInfoHeader'; + +let store; +let axiosMock; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const libraryData: ContentLibrary = { + id: 'lib:org1:lib1', + type: 'complex', + org: 'org1', + slug: 'lib1', + title: 'lib1', + description: 'lib1', + numBlocks: 2, + version: 0, + lastPublished: null, + lastDraftCreated: '2024-07-22', + publishedBy: 'staff', + lastDraftCreatedBy: 'staff', + allowLti: false, + allowPublicLearning: false, + allowPublicRead: false, + hasUnpublishedChanges: true, + hasUnpublishedDeletes: false, + canEditLibrary: true, + license: '', + created: '2024-06-26', + updated: '2024-07-20', +}; + +interface WrapperProps { + library?: ContentLibrary, +} + +const usageKey = 'lb:org1:library:html:a1fa8bdd-dc67-4976-9bf5-0ea75a9bca3d'; +const xBlockFields = { + display_name: 'Test HTML Block', + metadata: { + display_name: 'Test HTML Block', + }, +}; + +const RootWrapper = ({ library } : WrapperProps) => ( + + + + + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(getXBlockFieldsApiUrl(usageKey)).reply(200, xBlockFields); + }); + + afterEach(() => { + jest.clearAllMocks(); + axiosMock.restore(); + }); + + it('should render component info Header', async () => { + const { findByText, getByRole } = render(); + + expect(await findByText('Test HTML Block')).toBeInTheDocument(); + expect(getByRole('button', { name: /edit component name/i })).toBeInTheDocument(); + }); + + it('should not render edit title button without permission', () => { + const library = { + ...libraryData, + canEditLibrary: false, + }; + + const { queryByRole } = render(); + + expect(queryByRole('button', { name: /edit component name/i })).not.toBeInTheDocument(); + }); + + it('should edit component title', async () => { + const url = getXBlockFieldsApiUrl(usageKey); + axiosMock.onPost(url).reply(200); + const { getByRole, getByText } = render(); + + fireEvent.click(getByRole('button', { name: /edit component name/i })); + + const textBox = getByRole('textbox', { name: /display name input/i }); + + fireEvent.change(textBox, { target: { value: 'New component name' } }); + fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 }); + + expect(textBox).not.toBeInTheDocument(); + + 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 name' }, + })); + expect(getByText('Component updated successfully.')); + }); + }); + + it('should close edit library title on press Escape', async () => { + const url = getXBlockFieldsApiUrl(usageKey); + axiosMock.onPost(url).reply(200); + const { getByRole } = render(); + + fireEvent.click(getByRole('button', { name: /edit component name/i })); + + const textBox = getByRole('textbox', { name: /display name input/i }); + + fireEvent.change(textBox, { target: { value: 'New component name' } }); + fireEvent.keyDown(textBox, { key: 'Escape', code: 'Escape', charCode: 27 }); + + expect(textBox).not.toBeInTheDocument(); + + await waitFor(() => expect(axiosMock.history.post.length).toEqual(0)); + }); + + it('should show error on edit library tittle', async () => { + const url = getXBlockFieldsApiUrl(usageKey); + axiosMock.onPatch(url).reply(500); + + const { getByRole, getByText } = render(); + + fireEvent.click(getByRole('button', { name: /edit component name/i })); + + const textBox = getByRole('textbox', { name: /display name input/i }); + + fireEvent.change(textBox, { target: { value: 'New component name' } }); + 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 name' }, + })); + expect(getByText('There was an error updating the component.')); + }); + }); +}); diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx new file mode 100644 index 0000000000..6f7a5d6f2a --- /dev/null +++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx @@ -0,0 +1,98 @@ +/* eslint-disable react/require-default-props */ +import React, { useState, useContext, useCallback } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Icon, + IconButton, + Stack, + Form, +} from '@openedx/paragon'; +import { Edit } from '@openedx/paragon/icons'; + +import { ToastContext } from '../../generic/toast-context'; +import type { ContentLibrary } from '../data/api'; +import { useUpdateXBlockFields, useXBlockFields } from '../data/apiHooks'; +import messages from './messages'; + +interface ComponentInfoHeaderProps { + library: ContentLibrary; + usageKey: string; +} + +const ComponentInfoHeader = ({ library, usageKey }: ComponentInfoHeaderProps) => { + const intl = useIntl(); + const [inputIsActive, setIsActive] = useState(false); + + const { + data: xblockFields, + } = useXBlockFields(library.id, usageKey); + + const updateMutation = useUpdateXBlockFields(library.id, usageKey); + const { showToast } = useContext(ToastContext); + + const handleSaveDisplayName = useCallback( + (event) => { + const newDisplayName = event.target.value; + if (newDisplayName && newDisplayName !== xblockFields?.displayName) { + updateMutation.mutateAsync({ + metadata: { + display_name: newDisplayName, + }, + }).then(() => { + showToast(intl.formatMessage(messages.updateComponentSuccessMsg)); + }).catch(() => { + showToast(intl.formatMessage(messages.updateComponentErrorMsg)); + }); + } + setIsActive(false); + }, + [xblockFields, showToast, intl], + ); + + const handleClick = () => { + setIsActive(true); + }; + + const hanldeOnKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + handleSaveDisplayName(event); + } else if (event.key === 'Escape') { + setIsActive(false); + } + }; + + return ( + + { inputIsActive + ? ( + + ) + : ( + <> + + {xblockFields?.displayName} + + {library.canEditLibrary && ( + + )} + + )} + + ); +}; + +export default ComponentInfoHeader; diff --git a/src/library-authoring/component-info/index.tsx b/src/library-authoring/component-info/index.tsx new file mode 100644 index 0000000000..27bb2275f3 --- /dev/null +++ b/src/library-authoring/component-info/index.tsx @@ -0,0 +1,2 @@ +export { default as ComponentInfo } from './ComponentInfo'; +export { default as ComponentInfoHeader } from './ComponentInfoHeader'; diff --git a/src/library-authoring/component-info/messages.ts b/src/library-authoring/component-info/messages.ts new file mode 100644 index 0000000000..32251fce3c --- /dev/null +++ b/src/library-authoring/component-info/messages.ts @@ -0,0 +1,50 @@ +import { defineMessages as _defineMessages } from '@edx/frontend-platform/i18n'; +import type { defineMessages as defineMessagesType } from 'react-intl'; + +// frontend-platform currently doesn't provide types... do it ourselves. +const defineMessages = _defineMessages as typeof defineMessagesType; + +const messages = defineMessages({ + editNameButtonAlt: { + id: 'course-authoring.library-authoring.component.edit-name.alt', + defaultMessage: 'Edit component name', + description: 'Alt text for edit component name icon button', + }, + updateComponentSuccessMsg: { + id: 'course-authoring.library-authoring.component.update.success', + defaultMessage: 'Component updated successfully.', + description: 'Message when the component is updated successfully', + }, + updateComponentErrorMsg: { + id: 'course-authoring.library-authoring.component.update.error', + defaultMessage: 'There was an error updating the component.', + description: 'Message when there is an error when updating the component', + }, + editComponentButtonTitle: { + id: 'course-authoring.library-authoring.component.edit.title', + defaultMessage: 'Edit component', + description: 'Title for edit component button', + }, + publishComponentButtonTitle: { + id: 'course-authoring.library-authoring.component.publish.title', + defaultMessage: 'Publish component', + description: 'Title for publish component button', + }, + previewTabTitle: { + id: 'course-authoring.library-authoring.component.preview-tab.title', + defaultMessage: 'Preview', + description: 'Title for preview tab', + }, + manageTabTitle: { + id: 'course-authoring.library-authoring.component.manage-tab.title', + defaultMessage: 'Manage', + description: 'Title for manage tab', + }, + detailsTabTitle: { + id: 'course-authoring.library-authoring.component.details-tab.title', + defaultMessage: 'Details', + description: 'Title for details tab', + }, +}); + +export default messages; diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index a24df20de6..fc92a2b78b 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -16,6 +16,7 @@ import { updateClipboard } from '../../generic/data/api'; import TagCount from '../../generic/tag-count'; import { ToastContext } from '../../generic/toast-context'; import { type ContentHit, Highlight } from '../../search-manager'; +import { LibraryContext } from '../common/context'; import messages from './messages'; import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants'; @@ -24,7 +25,7 @@ type ComponentCardProps = { blockTypeDisplayName: string, }; -const ComponentCardMenu = ({ usageKey }: { usageKey: string }) => { +export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const intl = useIntl(); const { showToast } = useContext(ToastContext); const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL)); @@ -38,7 +39,7 @@ const ComponentCardMenu = ({ usageKey }: { usageKey: string }) => { }; return ( - + e.stopPropagation()}> { }; const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps) => { + const { + openComponentInfoSidebar, + } = useContext(LibraryContext); + const { blockType, formatted, @@ -84,7 +89,10 @@ const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps return ( - + openComponentInfoSidebar(usageKey)} + > - + )} /> diff --git a/src/library-authoring/components/index.ts b/src/library-authoring/components/index.ts index 63c42720e0..3a928498c7 100644 --- a/src/library-authoring/components/index.ts +++ b/src/library-authoring/components/index.ts @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export { default as LibraryComponents } from './LibraryComponents'; +export { ComponentMenu } from './ComponentCard'; diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 4b933833c0..e622e6addf 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -24,6 +24,10 @@ export const getCommitLibraryChangesUrl = (libraryId: string) => `${getApiBaseUr * Get the URL for paste clipboard content into library. */ export const getLibraryPasteClipboardUrl = (libraryId: string) => `${getApiBaseUrl()}/api/libraries/v2/${libraryId}/paste_clipboard/`; +/** + * Get the URL for the xblock metadata API. + */ +export const getXBlockFieldsApiUrl = (usageKey: string) => `${getApiBaseUrl()}/api/xblock/v2/xblocks/${usageKey}/fields/`; export interface ContentLibrary { id: string; @@ -64,6 +68,12 @@ export interface LibrariesV2Response { results: ContentLibrary[], } +export interface XBlockFields { + displayName: string; + metadata: Record; + data: string; +} + /* Additional custom parameters for the API request. */ export interface GetLibrariesV2CustomParams { /* (optional) Library type, default `complex` */ @@ -110,6 +120,13 @@ export interface LibraryPasteClipboardRequest { blockId: string; } +export interface UpdateXBlockFieldsRequest { + data?: unknown; + metadata?: { + display_name?: string; + }; +} + /** * Fetch block types of a library */ @@ -211,3 +228,19 @@ export async function libraryPasteClipboard({ ); return data; } + +/** + * Fetch xblock fields. + */ +export async function getXBlockFields(usageKey: string): Promise { + const { data } = await getAuthenticatedHttpClient().get(getXBlockFieldsApiUrl(usageKey)); + return camelCaseObject(data); +} + +/** + * Update xblock fields. + */ +export async function updateXBlockFields(usageKey:string, xblockData: UpdateXBlockFieldsRequest) { + const client = getAuthenticatedHttpClient(); + await client.post(getXBlockFieldsApiUrl(usageKey), xblockData); +} diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 64420a71bb..7cf9f7bd21 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -1,9 +1,13 @@ +import { camelCaseObject } from '@edx/frontend-platform'; import { - useQuery, useMutation, useQueryClient, Query, + useQuery, useMutation, useQueryClient, type Query, } from '@tanstack/react-query'; import { type GetLibrariesV2CustomParams, + type ContentLibrary, + type XBlockFields, + type UpdateXBlockFieldsRequest, getContentLibrary, getLibraryBlockTypes, createLibraryBlock, @@ -11,8 +15,9 @@ import { commitLibraryChanges, revertLibraryChanges, updateLibraryMetadata, - ContentLibrary, libraryPasteClipboard, + getXBlockFields, + updateXBlockFields, } from './api'; export const libraryAuthoringQueryKeys = { @@ -32,6 +37,13 @@ export const libraryAuthoringQueryKeys = { 'content', 'libraryBlockTypes', ], + xblockFields: (contentLibraryId: string, usageKey: string) => [ + ...libraryAuthoringQueryKeys.all, + ...libraryAuthoringQueryKeys.contentLibrary(contentLibraryId), + 'content', + 'xblockFields', + usageKey, + ], }; /** @@ -154,3 +166,46 @@ export const useLibraryPasteClipboard = () => { }, }); }; + +export const useXBlockFields = (contentLibrayId: string, usageKey: string) => ( + useQuery({ + queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibrayId, usageKey), + queryFn: () => getXBlockFields(usageKey), + enabled: !!usageKey, + }) +); + +export const useUpdateXBlockFields = (contentLibraryId: string, usageKey: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: UpdateXBlockFieldsRequest) => updateXBlockFields(usageKey, data), + onMutate: async (data) => { + const queryKey = libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey); + const previousBlockData = queryClient.getQueriesData(queryKey)[0][1] as XBlockFields; + const formatedData = camelCaseObject(data); + + const newBlockData = { + ...previousBlockData, + ...(formatedData.metadata?.displayName && { displayName: formatedData.metadata.displayName }), + metadata: { + ...previousBlockData.metadata, + ...formatedData.metadata, + }, + }; + + queryClient.setQueryData(queryKey, newBlockData); + + return { previousBlockData, newBlockData }; + }, + onError: (_err, _data, context) => { + queryClient.setQueryData( + libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey), + context?.previousBlockData, + ); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey) }); + queryClient.invalidateQueries({ queryKey: ['content_search'] }); + }, + }); +}; diff --git a/src/library-authoring/library-info/LibraryInfoHeader.tsx b/src/library-authoring/library-info/LibraryInfoHeader.tsx index e10fe2ec65..2f8c1ca532 100644 --- a/src/library-authoring/library-info/LibraryInfoHeader.tsx +++ b/src/library-authoring/library-info/LibraryInfoHeader.tsx @@ -8,7 +8,7 @@ import { import { Edit } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -import { ContentLibrary } from '../data/api'; +import type { ContentLibrary } from '../data/api'; import { useUpdateLibraryMetadata } from '../data/apiHooks'; import { ToastContext } from '../../generic/toast-context'; diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx index 314de8792a..a1e016dc92 100644 --- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx +++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx @@ -10,6 +10,7 @@ import messages from '../messages'; import { AddContentContainer, AddContentHeader } from '../add-content'; import { LibraryContext, SidebarBodyComponentId } from '../common/context'; import { LibraryInfo, LibraryInfoHeader } from '../library-info'; +import { ComponentInfo, ComponentInfoHeader } from '../component-info'; import { ContentLibrary } from '../data/api'; type LibrarySidebarProps = { @@ -27,25 +28,35 @@ type LibrarySidebarProps = { */ const LibrarySidebar = ({ library }: LibrarySidebarProps) => { const intl = useIntl(); - const { sidebarBodyComponent, closeLibrarySidebar } = useContext(LibraryContext); + const { + sidebarBodyComponent, + closeLibrarySidebar, + currentComponentUsageKey, + } = useContext(LibraryContext); const bodyComponentMap = { [SidebarBodyComponentId.AddContent]: , [SidebarBodyComponentId.Info]: , + [SidebarBodyComponentId.ComponentInfo]: ( + currentComponentUsageKey && + ), unknown: null, }; const headerComponentMap = { - 'add-content': , - info: , + [SidebarBodyComponentId.AddContent]: , + [SidebarBodyComponentId.Info]: , + [SidebarBodyComponentId.ComponentInfo]: ( + currentComponentUsageKey && + ), unknown: null, }; - const buildBody = () : React.ReactNode | null => bodyComponentMap[sidebarBodyComponent || 'unknown']; - const buildHeader = (): React.ReactNode | null => headerComponentMap[sidebarBodyComponent || 'unknown']; + const buildBody = () : React.ReactNode => bodyComponentMap[sidebarBodyComponent || 'unknown']; + const buildHeader = (): React.ReactNode => headerComponentMap[sidebarBodyComponent || 'unknown']; return ( - + {buildHeader()} Date: Thu, 22 Aug 2024 14:41:23 -0300 Subject: [PATCH 02/13] fix: invalidate only contex_search queries for this library --- src/library-authoring/data/apiHooks.ts | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index 7cf9f7bd21..2ebed19ff9 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -20,6 +20,21 @@ import { updateXBlockFields, } from './api'; +const libraryQueryPredicate = (query: Query, libraryId: string): boolean => { + // Invalidate all content queries related to this library. + // If we allow searching "all courses and libraries" in the future, + // then we'd have to invalidate all `["content_search", "results"]` + // queries, and not just the ones for this library, because items from + // this library could be included in an "all courses and libraries" + // search. For now we only allow searching individual libraries. + const extraFilter = query.queryKey[5]; // extraFilter contains library id + if (!(Array.isArray(extraFilter) || typeof extraFilter === 'string')) { + return false; + } + + return query.queryKey[0] === 'content_search' && extraFilter?.includes(`context_key = "${libraryId}"`); +}; + export const libraryAuthoringQueryKeys = { all: ['contentLibrary'], /** @@ -136,22 +151,7 @@ export const useRevertLibraryChanges = () => { mutationFn: revertLibraryChanges, onSettled: (_data, _error, libraryId) => { queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) }); - queryClient.invalidateQueries({ - // Invalidate all content queries related to this library. - // If we allow searching "all courses and libraries" in the future, - // then we'd have to invalidate all `["content_search", "results"]` - // queries, and not just the ones for this library, because items from - // this library could be included in an "all courses and libraries" - // search. For now we only allow searching individual libraries. - predicate: /* istanbul ignore next */ (query: Query): boolean => { - // extraFilter contains library id - const extraFilter = query.queryKey[5]; - if (!(Array.isArray(extraFilter) || typeof extraFilter === 'string')) { - return false; - } - return query.queryKey[0] === 'content_search' && extraFilter?.includes(`context_key = "${libraryId}"`); - }, - }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); }, }); }; @@ -162,7 +162,7 @@ export const useLibraryPasteClipboard = () => { mutationFn: libraryPasteClipboard, onSettled: (_data, _error, variables) => { queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.libraryId) }); - queryClient.invalidateQueries({ queryKey: ['content_search'] }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, variables.libraryId) }); }, }); }; @@ -205,7 +205,7 @@ export const useUpdateXBlockFields = (contentLibraryId: string, usageKey: string }, onSettled: () => { queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.xblockFields(contentLibraryId, usageKey) }); - queryClient.invalidateQueries({ queryKey: ['content_search'] }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) }); }, }); }; From b2a05d53ee2d80642420c5b7b799ffed3a9f9bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 22 Aug 2024 14:57:47 -0300 Subject: [PATCH 03/13] fix: add keydown to ComponentCard --- src/library-authoring/components/ComponentCard.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index fc92a2b78b..f460bc3ba4 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -92,6 +92,11 @@ const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps openComponentInfoSidebar(usageKey)} + onKeyDown={(e: React.KeyboardEvent) => { + if (['Enter', ' '].includes(e.key)) { + openComponentInfoSidebar(usageKey); + } + }} > Date: Fri, 23 Aug 2024 17:00:03 -0300 Subject: [PATCH 04/13] fix: remove unecessary eslint-disable --- src/library-authoring/component-info/ComponentInfo.tsx | 1 - src/library-authoring/component-info/ComponentInfoHeader.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 62bcc0387d..88e7b27533 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx index 6f7a5d6f2a..8f576fe1be 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import React, { useState, useContext, useCallback } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { From 1abf0598e1797ba24aa50b464959d71862d3ab9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Aug 2024 17:12:45 -0300 Subject: [PATCH 05/13] test: use screen and fix some tests --- .../ComponentInfoHeader.test.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx index ea7d794b83..a66b57b56e 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.test.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.test.tsx @@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render, fireEvent, + screen, waitFor, } from '@testing-library/react'; import { ContentLibrary, getXBlockFieldsApiUrl } from '../data/api'; @@ -95,10 +96,10 @@ describe('', () => { }); it('should render component info Header', async () => { - const { findByText, getByRole } = render(); + render(); - expect(await findByText('Test HTML Block')).toBeInTheDocument(); - expect(getByRole('button', { name: /edit component name/i })).toBeInTheDocument(); + expect(await screen.findByText('Test HTML Block')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /edit component name/i })).toBeInTheDocument(); }); it('should not render edit title button without permission', () => { @@ -107,19 +108,19 @@ describe('', () => { canEditLibrary: false, }; - const { queryByRole } = render(); + render(); - expect(queryByRole('button', { name: /edit component name/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /edit component name/i })).not.toBeInTheDocument(); }); it('should edit component title', async () => { const url = getXBlockFieldsApiUrl(usageKey); axiosMock.onPost(url).reply(200); - const { getByRole, getByText } = render(); + render(); - fireEvent.click(getByRole('button', { name: /edit component name/i })); + fireEvent.click(screen.getByRole('button', { name: /edit component name/i })); - const textBox = getByRole('textbox', { name: /display name input/i }); + const textBox = screen.getByRole('textbox', { name: /display name input/i }); fireEvent.change(textBox, { target: { value: 'New component name' } }); fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 }); @@ -132,18 +133,18 @@ describe('', () => { expect(axiosMock.history.post[0].data).toStrictEqual(JSON.stringify({ metadata: { display_name: 'New component name' }, })); - expect(getByText('Component updated successfully.')); + expect(screen.getByText('Component updated successfully.')).toBeInTheDocument(); }); }); it('should close edit library title on press Escape', async () => { const url = getXBlockFieldsApiUrl(usageKey); axiosMock.onPost(url).reply(200); - const { getByRole } = render(); + render(); - fireEvent.click(getByRole('button', { name: /edit component name/i })); + fireEvent.click(screen.getByRole('button', { name: /edit component name/i })); - const textBox = getByRole('textbox', { name: /display name input/i }); + const textBox = screen.getByRole('textbox', { name: /display name input/i }); fireEvent.change(textBox, { target: { value: 'New component name' } }); fireEvent.keyDown(textBox, { key: 'Escape', code: 'Escape', charCode: 27 }); @@ -157,11 +158,11 @@ describe('', () => { const url = getXBlockFieldsApiUrl(usageKey); axiosMock.onPatch(url).reply(500); - const { getByRole, getByText } = render(); + render(); - fireEvent.click(getByRole('button', { name: /edit component name/i })); + fireEvent.click(screen.getByRole('button', { name: /edit component name/i })); - const textBox = getByRole('textbox', { name: /display name input/i }); + const textBox = screen.getByRole('textbox', { name: /display name input/i }); fireEvent.change(textBox, { target: { value: 'New component name' } }); fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 }); @@ -172,7 +173,8 @@ describe('', () => { expect(axiosMock.history.post[0].data).toStrictEqual(JSON.stringify({ metadata: { display_name: 'New component name' }, })); - expect(getByText('There was an error updating the component.')); + + expect(screen.getByText('There was an error updating the component.')).toBeInTheDocument(); }); }); }); From ddf6f1ee98823ca2ded58bf31303b543c1f67593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Aug 2024 17:43:56 -0300 Subject: [PATCH 06/13] fix: improve responsiveness of library sidebar --- src/library-authoring/LibraryAuthoringPage.tsx | 4 ++-- src/library-authoring/component-info/ComponentInfo.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index c2eb969292..c8ffb58bed 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -197,8 +197,8 @@ const LibraryAuthoringPage = () => { - { sidebarBodyComponent !== null && ( - + { !!sidebarBodyComponent && ( + )} diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 88e7b27533..03c65d4b73 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -19,7 +19,7 @@ const ComponentInfo = ({ usageKey } : ComponentInfoProps) => { return ( - + From 06a3e9576be8c766e231d11bebadc7f03c0d8665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Fri, 23 Aug 2024 18:12:56 -0300 Subject: [PATCH 07/13] chore: trigger CI From 8aa14b1a8627d697d747478ab04c8c221ba31744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 26 Aug 2024 12:36:14 -0300 Subject: [PATCH 08/13] fix: improve responsive layout --- .../LibraryAuthoringPage.tsx | 138 +++++++++--------- .../components/LibraryComponents.tsx | 2 +- .../library-sidebar/LibrarySidebar.tsx | 2 +- 3 files changed, 72 insertions(+), 70 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index c8ffb58bed..f75d3e337a 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -132,78 +132,80 @@ const LibraryAuthoringPage = () => { }; return ( - - - -
- - - } - subtitle={intl.formatMessage(messages.headingSubtitle)} - headerActions={} - /> - -
- - - -
- -
- +
+ + + + + - - - - - - - )} - /> - } - /> - } + } + subtitle={intl.formatMessage(messages.headingSubtitle)} + headerActions={} /> - } - /> - + +
+ + + +
+ +
+ + + + + + + + )} + /> + } + /> + } + /> + } + /> + + - - - - { !!sidebarBodyComponent && ( - - - )} - - + { !!sidebarBodyComponent && ( + + + + )} + + + + ); }; diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 24140abaca..4065826428 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -72,7 +72,7 @@ const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => { { const buildHeader = (): React.ReactNode => headerComponentMap[sidebarBodyComponent || 'unknown']; return ( - + {buildHeader()} Date: Mon, 26 Aug 2024 13:05:13 -0300 Subject: [PATCH 09/13] test: update tests --- src/library-authoring/LibraryAuthoringPage.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index e0ce39b3e3..7747b998c9 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -175,8 +175,7 @@ describe('', () => { // Ensure the search endpoint is called: // Call 1: To fetch searchable/filterable/sortable library data - // Call 2: To fetch the recently modified components only - await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); + await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); }); return result; }; @@ -508,7 +507,7 @@ describe('', () => { // Re-selecting the previous sort option resets sort to default "Recently Modified" await testSortOption('Recently Published', 'modified:desc', true); - expect(getAllByText('Recently Modified').length).toEqual(2); + expect(getAllByText('Recently Modified').length).toEqual(3); // Enter a keyword into the search box const searchBox = getByRole('searchbox'); From 3c7007e2bf0f97faf8a4a923b052a92390f20ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 26 Aug 2024 14:56:18 -0300 Subject: [PATCH 10/13] fix: add max width to sidebar and prevent button text to wrap --- src/library-authoring/LibraryAuthoringPage.tsx | 2 +- src/library-authoring/component-info/ComponentInfo.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index f75d3e337a..7eca6fc04b 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -198,7 +198,7 @@ const LibraryAuthoringPage = () => { { !!sidebarBodyComponent && ( - + )} diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 03c65d4b73..4234722687 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -19,15 +19,15 @@ const ComponentInfo = ({ usageKey } : ComponentInfoProps) => { return ( - - - - +
Date: Mon, 26 Aug 2024 15:36:00 -0300 Subject: [PATCH 11/13] fix: typo --- src/library-authoring/LibraryAuthoringPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 7eca6fc04b..6a70cc4a21 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -198,7 +198,7 @@ const LibraryAuthoringPage = () => {
{ !!sidebarBodyComponent && ( - + )} From 6b679206a87eaf4454ca27eb77f17b3e040f025c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Mon, 26 Aug 2024 17:59:20 -0300 Subject: [PATCH 12/13] test: fix test --- src/library-authoring/LibraryAuthoringPage.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 6873a2f982..52f4694366 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -531,10 +531,11 @@ describe('', () => { it('should open and close the component sidebar', async () => { const usageKey = mockResult.results[0].hits[0].usage_key; - const { getAllByText, queryByTestId } = await renderLibraryPage(); + const { getAllByText, queryByTestId, queryByText } = await renderLibraryPage(); axiosMock.onGet(getXBlockFieldsApiUrl(usageKey)).reply(200, xBlockFields); // Click on the first component + waitFor(() => expect(queryByText('Test HTML Block')).toBeInTheDocument()); fireEvent.click(getAllByText('Test HTML Block')[0]); const sidebar = screen.getByTestId('library-sidebar'); From de62eba78b4147dc137e798f8d353c9be4e09e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 27 Aug 2024 20:39:12 -0300 Subject: [PATCH 13/13] fix: sidebar position and responsiveness --- .../LibraryAuthoringPage.scss | 6 + .../LibraryAuthoringPage.tsx | 144 +++++++++--------- .../library-sidebar/LibrarySidebar.tsx | 2 +- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.scss b/src/library-authoring/LibraryAuthoringPage.scss index 3ce8cb718a..eaf87428b8 100644 --- a/src/library-authoring/LibraryAuthoringPage.scss +++ b/src/library-authoring/LibraryAuthoringPage.scss @@ -9,3 +9,9 @@ } } } + +.library-authoring-sidebar { + min-width: 300px; + max-width: map-get($grid-breakpoints, "sm"); + z-index: 1001; // to appear over header +} diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 6671c40f90..5f8c144d34 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -5,9 +5,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { Badge, Button, - Col, Container, - Row, Stack, Tab, Tabs, @@ -152,80 +150,76 @@ const LibraryAuthoringPage = () => { }; return ( - <> -
- - - - - - } - subtitle={intl.formatMessage(messages.headingSubtitle)} - headerActions={} - /> - -
- - - -
- -
- - - - - - - - )} +
+
+
+ + + } + subtitle={intl.formatMessage(messages.headingSubtitle)} + headerActions={} + /> + +
+ + + +
+ +
+ + + + + + + - } - /> - } - /> - } - /> - - - - - { !!sidebarBodyComponent && ( - - - - )} - - - - + )} + /> + } + /> + } + /> + } + /> + + + + +
+ { !!sidebarBodyComponent && ( +
+ +
+ )} +
); }; diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx index fba487a270..64d57838da 100644 --- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx +++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx @@ -56,7 +56,7 @@ const LibrarySidebar = ({ library }: LibrarySidebarProps) => { const buildHeader = (): React.ReactNode => headerComponentMap[sidebarBodyComponent || 'unknown']; return ( - + {buildHeader()}