From 2f8717edf0a79bdaef2490e71fe5cc10971d37ae Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Fri, 27 Sep 2024 10:40:25 -0700 Subject: [PATCH] refactor: simplify Library Context --- src/library-authoring/EmptyStates.tsx | 4 +- .../LibraryAuthoringPage.tsx | 24 ++++------ src/library-authoring/LibraryHome.tsx | 12 +++-- .../LibraryRecentlyModified.tsx | 23 ++++++---- .../add-content/AddContentContainer.tsx | 4 +- .../LibraryCollectionComponents.tsx | 9 ++-- .../collections/LibraryCollectionPage.tsx | 10 ++--- .../collections/LibraryCollections.tsx | 6 +-- src/library-authoring/common/context.tsx | 45 +++++++++++++------ .../components/ComponentCard.tsx | 4 +- .../components/LibraryComponents.tsx | 11 +++-- .../CreateCollectionModal.tsx | 13 +++--- .../library-sidebar/LibrarySidebar.tsx | 6 +-- 13 files changed, 89 insertions(+), 82 deletions(-) diff --git a/src/library-authoring/EmptyStates.tsx b/src/library-authoring/EmptyStates.tsx index eea5ed732a..9470f0ad5c 100644 --- a/src/library-authoring/EmptyStates.tsx +++ b/src/library-authoring/EmptyStates.tsx @@ -1,4 +1,3 @@ -import { useParams } from 'react-router'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import type { MessageDescriptor } from 'react-intl'; import { @@ -8,6 +7,7 @@ import { Add } from '@openedx/paragon/icons'; import { ClearFiltersButton } from '../search-manager'; import messages from './messages'; import { useContentLibrary } from './data/apiHooks'; +import { useLibraryContext } from './common/context'; export const NoComponents = ({ infoText = messages.noComponents, @@ -18,7 +18,7 @@ export const NoComponents = ({ addBtnText?: MessageDescriptor; handleBtnClick: () => void; }) => { - const { libraryId } = useParams(); + const { libraryId } = useLibraryContext(); const { data: libraryData } = useContentLibrary(libraryId); const canEditLibrary = libraryData?.canEditLibrary ?? false; diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 343bb94f62..9ca4d757c4 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Helmet } from 'react-helmet'; import classNames from 'classnames'; import { StudioFooter } from '@edx/frontend-component-footer'; @@ -13,7 +13,7 @@ import { } from '@openedx/paragon'; import { Add, InfoOutline } from '@openedx/paragon/icons'; import { - Routes, Route, useLocation, useNavigate, useParams, useSearchParams, + Routes, Route, useLocation, useNavigate, useSearchParams, } from 'react-router-dom'; import Loading from '../generic/Loading'; @@ -33,7 +33,7 @@ import LibraryCollections from './collections/LibraryCollections'; import LibraryHome from './LibraryHome'; import { useContentLibrary } from './data/apiHooks'; import { LibrarySidebar } from './library-sidebar'; -import { LibraryContext, SidebarBodyComponentId } from './common/context'; +import { SidebarBodyComponentId, useLibraryContext } from './common/context'; import messages from './messages'; enum TabList { @@ -53,7 +53,7 @@ const HeaderActions = ({ canEditLibrary }: HeaderActionsProps) => { openInfoSidebar, closeLibrarySidebar, sidebarBodyComponent, - } = useContext(LibraryContext); + } = useLibraryContext(); if (!canEditLibrary) { return null; @@ -119,11 +119,7 @@ const LibraryAuthoringPage = () => { const location = useLocation(); const navigate = useNavigate(); - const { libraryId } = useParams(); - if (!libraryId) { - // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. - throw new Error('Rendered without libraryId URL parameter'); - } + const { libraryId } = useLibraryContext(); const { data: libraryData, isLoading } = useContentLibrary(libraryId); const currentPath = location.pathname.split('/').pop(); @@ -131,7 +127,7 @@ const LibraryAuthoringPage = () => { const { sidebarBodyComponent, openInfoSidebar, - } = useContext(LibraryContext); + } = useLibraryContext(); useEffect(() => { openInfoSidebar(); @@ -196,16 +192,12 @@ const LibraryAuthoringPage = () => { + )} /> } + element={} /> void, }; -const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) => { +const LibraryHome = ({ tabList, handleTabChange } : LibraryHomeProps) => { const intl = useIntl(); const { totalHits: componentCount, totalCollectionHits: collectionCount, isFiltered, } = useSearchContext(); - const { openAddContentSidebar } = useContext(LibraryContext); + const { openAddContentSidebar } = useLibraryContext(); const renderEmptyState = () => { if (componentCount === 0 && collectionCount === 0) { @@ -35,7 +33,7 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) return ( - + { renderEmptyState() || ( @@ -52,7 +50,7 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) contentCount={componentCount} viewAllAction={() => handleTabChange(tabList.components)} > - + ) diff --git a/src/library-authoring/LibraryRecentlyModified.tsx b/src/library-authoring/LibraryRecentlyModified.tsx index 57828871ef..e6ef7ff7de 100644 --- a/src/library-authoring/LibraryRecentlyModified.tsx +++ b/src/library-authoring/LibraryRecentlyModified.tsx @@ -10,8 +10,9 @@ import messages from './messages'; import ComponentCard from './components/ComponentCard'; import { useLibraryBlockTypes } from './data/apiHooks'; import CollectionCard from './components/CollectionCard'; +import { useLibraryContext } from './common/context'; -const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => { +const RecentlyModified: React.FC> = () => { const intl = useIntl(); const { hits, @@ -19,6 +20,7 @@ const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => { totalHits, totalCollectionHits, } = useSearchContext(); + const { libraryId } = useLibraryContext(); const componentCount = totalHits + totalCollectionHits; // Since we only display a fixed number of items in preview, @@ -77,13 +79,16 @@ const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => { : null; }; -const LibraryRecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => ( - - - -); +const LibraryRecentlyModified: React.FC> = () => { + const { libraryId } = useLibraryContext(); + return ( + + + + ); +}; export default LibraryRecentlyModified; diff --git a/src/library-authoring/add-content/AddContentContainer.tsx b/src/library-authoring/add-content/AddContentContainer.tsx index 1d46aaacbb..fe88ba0cdc 100644 --- a/src/library-authoring/add-content/AddContentContainer.tsx +++ b/src/library-authoring/add-content/AddContentContainer.tsx @@ -23,9 +23,9 @@ import { useCopyToClipboard } from '../../generic/clipboard'; import { getCanEdit } from '../../course-unit/data/selectors'; import { useCreateLibraryBlock, useLibraryPasteClipboard, useUpdateCollectionComponents } from '../data/apiHooks'; import { getEditUrl } from '../components/utils'; +import { useLibraryContext } from '../common/context'; import messages from './messages'; -import { LibraryContext } from '../common/context'; type ContentType = { name: string, @@ -73,7 +73,7 @@ const AddContentContainer = () => { const { showPasteXBlock } = useCopyToClipboard(canEdit); const { openCreateCollectionModal, - } = React.useContext(LibraryContext); + } = useLibraryContext(); const collectionButtonData = { name: intl.formatMessage(messages.collectionButton), diff --git a/src/library-authoring/collections/LibraryCollectionComponents.tsx b/src/library-authoring/collections/LibraryCollectionComponents.tsx index 5d870645c9..27ffd5639c 100644 --- a/src/library-authoring/collections/LibraryCollectionComponents.tsx +++ b/src/library-authoring/collections/LibraryCollectionComponents.tsx @@ -1,14 +1,13 @@ -import { useContext } from 'react'; import { Stack } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useSearchContext } from '../../search-manager'; import { LibraryComponents } from '../components'; import messages from './messages'; -import { LibraryContext } from '../common/context'; +import { useLibraryContext } from '../common/context'; -const LibraryCollectionComponents = ({ libraryId }: { libraryId: string }) => { +const LibraryCollectionComponents = () => { const { totalHits: componentCount, isFiltered } = useSearchContext(); - const { openAddContentSidebar } = useContext(LibraryContext); + const { openAddContentSidebar } = useLibraryContext(); if (componentCount === 0) { return isFiltered @@ -25,7 +24,7 @@ const LibraryCollectionComponents = ({ libraryId }: { libraryId: string }) => { return (

Content ({componentCount})

- +
); }; diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index b2344a9b1f..7e08a8d546 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -28,7 +28,7 @@ import { useSearchContext, } from '../../search-manager'; import { useContentLibrary } from '../data/apiHooks'; -import { LibraryContext } from '../common/context'; +import { useLibraryContext } from '../common/context'; import messages from './messages'; import { LibrarySidebar } from '../library-sidebar'; import LibraryCollectionComponents from './LibraryCollectionComponents'; @@ -37,7 +37,7 @@ const HeaderActions = ({ canEditLibrary }: { canEditLibrary: boolean; }) => { const intl = useIntl(); const { openAddContentSidebar, - } = useContext(LibraryContext); + } = useLibraryContext(); if (!canEditLibrary) { return null; @@ -98,7 +98,7 @@ const LibraryCollectionPageInner = ({ libraryId }: { libraryId: string }) => { const { sidebarBodyComponent, openCollectionInfoSidebar, - } = useContext(LibraryContext); + } = useLibraryContext(); const { collectionHits: [collectionData], isFetching } = useSearchContext(); useEffect(() => { @@ -169,7 +169,7 @@ const LibraryCollectionPageInner = ({ libraryId }: { libraryId: string }) => {
- + diff --git a/src/library-authoring/collections/LibraryCollections.tsx b/src/library-authoring/collections/LibraryCollections.tsx index 97d194f4a3..d68e2b16cf 100644 --- a/src/library-authoring/collections/LibraryCollections.tsx +++ b/src/library-authoring/collections/LibraryCollections.tsx @@ -1,12 +1,10 @@ -import { useContext } from 'react'; - import { useLoadOnScroll } from '../../hooks'; import { useSearchContext } from '../../search-manager'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import CollectionCard from '../components/CollectionCard'; import { LIBRARY_SECTION_PREVIEW_LIMIT } from '../components/LibrarySection'; import messages from './messages'; -import { LibraryContext } from '../common/context'; +import { useLibraryContext } from '../common/context'; type LibraryCollectionsProps = { variant: 'full' | 'preview', @@ -29,7 +27,7 @@ const LibraryCollections = ({ variant }: LibraryCollectionsProps) => { isFiltered, } = useSearchContext(); - const { openCreateCollectionModal } = useContext(LibraryContext); + const { openCreateCollectionModal } = useLibraryContext(); const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits; diff --git a/src/library-authoring/common/context.tsx b/src/library-authoring/common/context.tsx index cd82a2d84a..06d88caa33 100644 --- a/src/library-authoring/common/context.tsx +++ b/src/library-authoring/common/context.tsx @@ -1,5 +1,6 @@ import { useToggle } from '@openedx/paragon'; import React from 'react'; +import { useParams } from 'react-router-dom'; export enum SidebarBodyComponentId { AddContent = 'add-content', @@ -9,34 +10,42 @@ export enum SidebarBodyComponentId { } export interface LibraryContextData { + /** The ID of the current library */ + libraryId: string; + // Sidebar stuff - only one sidebar is active at any given time: sidebarBodyComponent: SidebarBodyComponentId | null; closeLibrarySidebar: () => void; openAddContentSidebar: () => void; openInfoSidebar: () => void; openComponentInfoSidebar: (usageKey: string) => void; + openCollectionInfoSidebar: () => void; currentComponentUsageKey?: string; + // "Create New Collection" modal isCreateCollectionModalOpen: boolean; openCreateCollectionModal: () => void; closeCreateCollectionModal: () => void; - openCollectionInfoSidebar: () => void; } -export const LibraryContext = React.createContext({ - sidebarBodyComponent: null, - closeLibrarySidebar: () => {}, - openAddContentSidebar: () => {}, - openInfoSidebar: () => {}, - openComponentInfoSidebar: (_usageKey: string) => {}, // eslint-disable-line @typescript-eslint/no-unused-vars - isCreateCollectionModalOpen: false, - openCreateCollectionModal: () => {}, - closeCreateCollectionModal: () => {}, - openCollectionInfoSidebar: () => {}, -} as LibraryContextData); +/** + * Library Context. + * Always available when we're in the context of a single library. + * + * Get this using `useLibraryContext()` + * + * Not used on the "library list" on Studio home. + */ +const LibraryContext = React.createContext(undefined); /** * React component to provide `LibraryContext` */ export const LibraryProvider = (props: { children?: React.ReactNode }) => { + const { libraryId } = useParams(); + + if (libraryId === undefined) { + // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. + throw new Error('Error: route is missing libraryId.'); + } const [sidebarBodyComponent, setSidebarBodyComponent] = React.useState(null); const [currentComponentUsageKey, setCurrentComponentUsageKey] = React.useState(); const [isCreateCollectionModalOpen, openCreateCollectionModal, closeCreateCollectionModal] = useToggle(false); @@ -65,7 +74,8 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => { setSidebarBodyComponent(SidebarBodyComponentId.CollectionInfo); }, []); - const context = React.useMemo(() => ({ + const context = React.useMemo(() => ({ + libraryId, sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar, @@ -77,6 +87,7 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => { closeCreateCollectionModal, openCollectionInfoSidebar, }), [ + libraryId, sidebarBodyComponent, closeLibrarySidebar, openAddContentSidebar, @@ -95,3 +106,11 @@ export const LibraryProvider = (props: { children?: React.ReactNode }) => { ); }; + +export function useLibraryContext(): LibraryContextData { + const ctx = React.useContext(LibraryContext); + if (ctx === undefined) { + throw new Error('useLibraryContext() was used in a component without a ancestor.'); + } + return ctx; +} diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 39a23926fc..a1314544d7 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -12,7 +12,7 @@ import { Link } from 'react-router-dom'; import { updateClipboard } from '../../generic/data/api'; import { ToastContext } from '../../generic/toast-context'; import { type ContentHit } from '../../search-manager'; -import { LibraryContext } from '../common/context'; +import { useLibraryContext } from '../common/context'; import messages from './messages'; import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants'; import { getEditUrl } from './utils'; @@ -66,7 +66,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps) => { const { openComponentInfoSidebar, - } = useContext(LibraryContext); + } = useLibraryContext(); const { blockType, diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index c91dbad55a..7d5280663f 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { useLoadOnScroll } from '../../hooks'; import { useSearchContext } from '../../search-manager'; @@ -6,10 +6,9 @@ import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useLibraryBlockTypes } from '../data/apiHooks'; import ComponentCard from './ComponentCard'; import { LIBRARY_SECTION_PREVIEW_LIMIT } from './LibrarySection'; -import { LibraryContext } from '../common/context'; +import { useLibraryContext } from '../common/context'; type LibraryComponentsProps = { - libraryId: string, variant: 'full' | 'preview', }; @@ -20,7 +19,7 @@ type LibraryComponentsProps = { * - 'full': Show all components with Infinite scroll pagination. * - 'preview': Show first 4 components without pagination. */ -const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => { +const LibraryComponents = ({ variant }: LibraryComponentsProps) => { const { hits, totalHits: componentCount, @@ -29,11 +28,11 @@ const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => { fetchNextPage, isFiltered, } = useSearchContext(); - const { openAddContentSidebar } = useContext(LibraryContext); + const { libraryId, openAddContentSidebar } = useLibraryContext(); const componentList = variant === 'preview' ? hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : hits; - // TODO add this to LibraryContext + // TODO get rid of "useLibraryBlockTypes". Use instead. const { data: blockTypesData } = useLibraryBlockTypes(libraryId); const blockTypes = useMemo(() => { const result = {}; diff --git a/src/library-authoring/create-collection/CreateCollectionModal.tsx b/src/library-authoring/create-collection/CreateCollectionModal.tsx index cc611b3a96..221f935043 100644 --- a/src/library-authoring/create-collection/CreateCollectionModal.tsx +++ b/src/library-authoring/create-collection/CreateCollectionModal.tsx @@ -5,12 +5,12 @@ import { Form, ModalDialog, } from '@openedx/paragon'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Formik } from 'formik'; import * as Yup from 'yup'; import FormikControl from '../../generic/FormikControl'; -import { LibraryContext } from '../common/context'; +import { useLibraryContext } from '../common/context'; import messages from './messages'; import { useCreateLibraryCollection } from '../data/apiHooks'; import { ToastContext } from '../../generic/toast-context'; @@ -18,15 +18,12 @@ import { ToastContext } from '../../generic/toast-context'; const CreateCollectionModal = () => { const intl = useIntl(); const navigate = useNavigate(); - const { libraryId } = useParams(); - if (!libraryId) { - throw new Error('Rendered without libraryId URL parameter'); - } - const create = useCreateLibraryCollection(libraryId!); const { + libraryId, isCreateCollectionModalOpen, closeCreateCollectionModal, - } = React.useContext(LibraryContext); + } = useLibraryContext(); + const create = useCreateLibraryCollection(libraryId!); const { showToast } = React.useContext(ToastContext); const handleCreate = React.useCallback((values) => { diff --git a/src/library-authoring/library-sidebar/LibrarySidebar.tsx b/src/library-authoring/library-sidebar/LibrarySidebar.tsx index d1ac43de22..126edda325 100644 --- a/src/library-authoring/library-sidebar/LibrarySidebar.tsx +++ b/src/library-authoring/library-sidebar/LibrarySidebar.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { Stack, Icon, @@ -8,7 +8,7 @@ import { Close } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from '../messages'; import { AddContentContainer, AddContentHeader } from '../add-content'; -import { LibraryContext, SidebarBodyComponentId } from '../common/context'; +import { SidebarBodyComponentId, useLibraryContext } from '../common/context'; import { LibraryInfo, LibraryInfoHeader } from '../library-info'; import { ComponentInfo, ComponentInfoHeader } from '../component-info'; import { ContentLibrary } from '../data/api'; @@ -35,7 +35,7 @@ const LibrarySidebar = ({ library, collection }: LibrarySidebarProps) => { sidebarBodyComponent, closeLibrarySidebar, currentComponentUsageKey, - } = useContext(LibraryContext); + } = useLibraryContext(); const bodyComponentMap = { [SidebarBodyComponentId.AddContent]: ,