From d5e36cf2b8f100e86869c07fcdf488d7bf181a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 7 May 2025 19:39:55 -0300 Subject: [PATCH] fix: unit pages ux bugs [FC-0083] (#1884) (#1916) This PR fixes some UX bugs related to the unit pages: * Sort for "recently modified" on unit tab does not update after adding new components to units * Change component delete warning message It's a backport of https://github.com/openedx/frontend-app-authoring/pull/1884 --- .../{DeleteModal.jsx => DeleteModal.tsx} | 43 +++++++------------ .../delete-modal/{messages.js => messages.ts} | 0 .../LibraryAuthoringPage.tsx | 2 +- .../components/ComponentDeleter.tsx | 30 ++++++++----- .../components/ContainerDeleter.tsx | 4 +- src/library-authoring/components/messages.ts | 12 +++++- src/library-authoring/data/apiHooks.ts | 17 ++++++-- 7 files changed, 61 insertions(+), 47 deletions(-) rename src/generic/delete-modal/{DeleteModal.jsx => DeleteModal.tsx} (71%) rename src/generic/delete-modal/{messages.js => messages.ts} (100%) diff --git a/src/generic/delete-modal/DeleteModal.jsx b/src/generic/delete-modal/DeleteModal.tsx similarity index 71% rename from src/generic/delete-modal/DeleteModal.jsx rename to src/generic/delete-modal/DeleteModal.tsx index 4159d9d3f..cf81b9636 100644 --- a/src/generic/delete-modal/DeleteModal.jsx +++ b/src/generic/delete-modal/DeleteModal.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { ActionRow, Button, @@ -9,17 +8,29 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import LoadingButton from '../loading-button'; +interface DeleteModalProps { + isOpen: boolean; + close: () => void; + category?: string; + onDeleteSubmit: () => void | Promise; + title?: string; + description?: React.ReactNode | React.ReactNode[]; + variant?: string; + btnLabel?: string; + icon?: React.ElementType; +} + const DeleteModal = ({ - category, + category = '', isOpen, close, onDeleteSubmit, title, description, - variant, + variant = 'default', btnLabel, icon, -}) => { +}: DeleteModalProps) => { const intl = useIntl(); const modalTitle = title || intl.formatMessage(messages.title, { category }); @@ -62,28 +73,4 @@ const DeleteModal = ({ ); }; -DeleteModal.defaultProps = { - category: '', - title: '', - description: '', - variant: 'default', - btnLabel: '', - icon: null, -}; - -DeleteModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - close: PropTypes.func.isRequired, - category: PropTypes.string, - onDeleteSubmit: PropTypes.func.isRequired, - title: PropTypes.string, - description: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string, - ]), - variant: PropTypes.string, - btnLabel: PropTypes.string, - icon: PropTypes.elementType, -}; - export default DeleteModal; diff --git a/src/generic/delete-modal/messages.js b/src/generic/delete-modal/messages.ts similarity index 100% rename from src/generic/delete-modal/messages.js rename to src/generic/delete-modal/messages.ts diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 6ffc4182d..95172f8d0 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -256,7 +256,7 @@ const LibraryAuthoringPage = ({ [ContentType.units]: intl.formatMessage(messages.unitsTab), }; const visibleTabsToRender = visibleTabs.map((contentType) => ( - + )); return ( diff --git a/src/library-authoring/components/ComponentDeleter.tsx b/src/library-authoring/components/ComponentDeleter.tsx index 2ea3a99e8..c48dedec1 100644 --- a/src/library-authoring/components/ComponentDeleter.tsx +++ b/src/library-authoring/components/ComponentDeleter.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useContext } from 'react'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { Warning } from '@openedx/paragon/icons'; +import { Icon } from '@openedx/paragon'; +import { CalendarViewDay, School, Warning } from '@openedx/paragon/icons'; import { useSidebarContext } from '../common/context/SidebarContext'; import { useDeleteLibraryBlock, useLibraryBlockMetadata, useRestoreLibraryBlock } from '../data/apiHooks'; @@ -66,6 +67,22 @@ const ComponentDeleter = ({ usageKey, ...props }: Props) => { return null; } + const deleteText = intl.formatMessage(messages.deleteComponentConfirm, { + componentName: , + message: ( + <> +
+ + {intl.formatMessage(messages.deleteComponentConfirmMsg1)} +
+
+ + {intl.formatMessage(messages.deleteComponentConfirmMsg2)} +
+ + ), + }); + return ( { variant="warning" title={intl.formatMessage(messages.deleteComponentWarningTitle)} icon={Warning} - description={( - - ), - }} - /> -)} + description={deleteText} onDeleteSubmit={doDelete} /> ); diff --git a/src/library-authoring/components/ContainerDeleter.tsx b/src/library-authoring/components/ContainerDeleter.tsx index 9b7d5db05..a4a1affc1 100644 --- a/src/library-authoring/components/ContainerDeleter.tsx +++ b/src/library-authoring/components/ContainerDeleter.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback, useContext } from 'react'; +import { useCallback, useContext } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon } from '@openedx/paragon'; import { Warning, School, Widgets } from '@openedx/paragon/icons'; @@ -47,7 +47,7 @@ const ContainerDeleter = ({ ), - }) as ReactNode as string; + }); const deleteSuccess = intl.formatMessage(messages.deleteUnitSuccess); const deleteError = intl.formatMessage(messages.deleteUnitFailed); const undoDeleteError = messages.undoDeleteUnitToastFailed; diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index 250793077..8248b4f43 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -68,9 +68,19 @@ const messages = defineMessages({ }, deleteComponentConfirm: { id: 'course-authoring.library-authoring.component.delete-confirmation-text', - defaultMessage: 'Delete {componentName}? If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.', + defaultMessage: 'Delete {componentName}? {message}', description: 'Confirmation text to display before deleting a component', }, + deleteComponentConfirmMsg1: { + id: 'course-authoring.library-authoring.component.delete-confirmation-msg-1', + defaultMessage: 'If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.', + description: 'First part of confirmation message to display before deleting a component', + }, + deleteComponentConfirmMsg2: { + id: 'course-authoring.library-authoring.component.delete-confirmation-msg-2', + defaultMessage: 'If this component has been used in any units, it will also be deleted from those units.', + description: 'Second part of confirmation message to display before deleting a component', + }, deleteComponentCancelButton: { id: 'course-authoring.library-authoring.component.cancel-delete-button', defaultMessage: 'Cancel', diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index e9a5202b1..b9a825b34 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -642,13 +642,22 @@ export const useAddComponentsToContainer = (containerId?: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (componentIds: string[]) => { - if (containerId !== undefined) { - return api.addComponentsToContainer(containerId, componentIds); + // istanbul ignore if: this should never happen + if (!containerId) { + return undefined; } - return undefined; + return api.addComponentsToContainer(containerId, componentIds); }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(containerId!) }); + // istanbul ignore if: this should never happen + if (!containerId) { + return; + } + // NOTE: We invalidate the library query here because we need to update the library's + // container list. + const libraryId = getLibraryId(containerId); + queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(containerId) }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); }, }); };