feat: allow editing imported unit blocks (#2405)
Allows authors to edit imported unit display name in outline.
This commit is contained in:
@@ -1,11 +1,6 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'authoring.proctoring.alert.error': {
|
||||
id: 'authoring.proctoring.alert.error',
|
||||
defaultMessage: 'We encountered a technical error while trying to save proctored exam settings. This might be a temporary issue, so please try again in a few minutes. If the problem persists, please go to the {support_link} for help.',
|
||||
description: 'Alert message for proctoring settings save error.',
|
||||
},
|
||||
'authoring.proctoring.alert.forbidden': {
|
||||
id: 'authoring.proctoring.alert.forbidden',
|
||||
defaultMessage: 'You do not have permission to edit proctored exam settings for this course. If you are a course team member and this problem persists, please go to the {support_link} for help.',
|
||||
|
||||
@@ -32,7 +32,7 @@ const messages = defineMessages({
|
||||
description: 'Tab title for review tab',
|
||||
},
|
||||
reviewTabDescriptionEmpty: {
|
||||
id: 'course-authoring.course-libraries.tab.home.description-no-links',
|
||||
id: 'course-authoring.course-libraries.tab.review.description-no-links',
|
||||
defaultMessage: 'All components are up to date',
|
||||
description: 'Description text for home tab',
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import CardHeader from './CardHeader';
|
||||
import TitleButton from './TitleButton';
|
||||
import messages from './messages';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const onExpandMock = jest.fn();
|
||||
const onClickMenuButtonMock = jest.fn();
|
||||
@@ -232,16 +233,6 @@ describe('<CardHeader />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('check is field disabled when isDisabledEditField is true', async () => {
|
||||
renderComponent({
|
||||
...cardHeaderProps,
|
||||
isFormOpen: true,
|
||||
isDisabledEditField: true,
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('subsection-edit-field')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('check editing is enabled when isDisabledEditField is false', async () => {
|
||||
renderComponent({ ...cardHeaderProps });
|
||||
|
||||
@@ -254,8 +245,8 @@ describe('<CardHeader />', () => {
|
||||
expect(await screen.findByTestId('subsection-card-header__menu-manage-tags-button')).not.toHaveAttribute('aria-disabled');
|
||||
});
|
||||
|
||||
it('check editing is disabled when isDisabledEditField is true', async () => {
|
||||
renderComponent({ ...cardHeaderProps, isDisabledEditField: true });
|
||||
it('check editing is disabled when saving is in progress', async () => {
|
||||
renderComponent({ ...cardHeaderProps, savingStatus: RequestStatus.IN_PROGRESS });
|
||||
|
||||
expect(await screen.findByTestId('subsection-edit-button')).toBeDisabled();
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ContentTagsDrawerSheet } from '@src/content-tags-drawer';
|
||||
import TagCount from '@src/generic/tag-count';
|
||||
import { useEscapeClick } from '@src/hooks';
|
||||
import { XBlockActions } from '@src/data/types';
|
||||
import { RequestStatus, RequestStatusType } from '@src/data/constants';
|
||||
import { ITEM_BADGE_STATUS } from '../constants';
|
||||
import { scrollToElement } from '../utils';
|
||||
import CardStatus from './CardStatus';
|
||||
@@ -41,7 +42,6 @@ interface CardHeaderProps {
|
||||
isFormOpen: boolean;
|
||||
onEditSubmit: (titleValue: string) => void;
|
||||
closeForm: () => void;
|
||||
isDisabledEditField: boolean;
|
||||
onClickDelete: () => void;
|
||||
onClickUnlink: () => void;
|
||||
onClickDuplicate: () => void;
|
||||
@@ -69,6 +69,7 @@ interface CardHeaderProps {
|
||||
extraActionsComponent?: ReactNode,
|
||||
onClickSync?: () => void;
|
||||
readyToSync?: boolean;
|
||||
savingStatus?: RequestStatusType;
|
||||
}
|
||||
|
||||
const CardHeader = ({
|
||||
@@ -83,7 +84,6 @@ const CardHeader = ({
|
||||
isFormOpen,
|
||||
onEditSubmit,
|
||||
closeForm,
|
||||
isDisabledEditField,
|
||||
onClickDelete,
|
||||
onClickUnlink,
|
||||
onClickDuplicate,
|
||||
@@ -103,6 +103,7 @@ const CardHeader = ({
|
||||
extraActionsComponent,
|
||||
onClickSync,
|
||||
readyToSync,
|
||||
savingStatus,
|
||||
}: CardHeaderProps) => {
|
||||
const intl = useIntl();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -119,6 +120,7 @@ const CardHeader = ({
|
||||
|| status === ITEM_BADGE_STATUS.publishedNotLive) && !hasChanges;
|
||||
|
||||
const { data: contentTagCount } = useContentTagsCount(cardId);
|
||||
const isSaving = savingStatus === RequestStatus.IN_PROGRESS;
|
||||
|
||||
useEffect(() => {
|
||||
const locatorId = searchParams.get('show');
|
||||
@@ -172,7 +174,7 @@ const CardHeader = ({
|
||||
onEditSubmit(titleValue);
|
||||
}
|
||||
}}
|
||||
disabled={isDisabledEditField}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</Form.Group>
|
||||
) : (
|
||||
@@ -186,7 +188,7 @@ const CardHeader = ({
|
||||
iconAs={EditIcon}
|
||||
onClick={onClickEdit}
|
||||
// @ts-ignore
|
||||
disabled={isDisabledEditField}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -238,7 +240,7 @@ const CardHeader = ({
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-configure-button`}
|
||||
disabled={isDisabledEditField}
|
||||
disabled={isSaving}
|
||||
onClick={onClickConfigure}
|
||||
>
|
||||
{intl.formatMessage(messages.menuConfigure)}
|
||||
@@ -246,7 +248,7 @@ const CardHeader = ({
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-manage-tags-button`}
|
||||
disabled={isDisabledEditField}
|
||||
disabled={isSaving}
|
||||
onClick={openManageTagsDrawer}
|
||||
>
|
||||
{intl.formatMessage(messages.menuManageTags)}
|
||||
|
||||
@@ -88,7 +88,6 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
|
||||
onOpenDeleteModal={jest.fn()}
|
||||
onOpenUnlinkModal={jest.fn()}
|
||||
onOpenConfigureModal={jest.fn()}
|
||||
savingStatus=""
|
||||
onEditSectionSubmit={onEditSectionSubmit}
|
||||
onDuplicateSubmit={jest.fn()}
|
||||
isSectionsExpanded
|
||||
|
||||
@@ -11,7 +11,7 @@ import classNames from 'classnames';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { setCurrentItem, setCurrentSection } from '@src/course-outline/data/slice';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import { RequestStatus, RequestStatusType } from '@src/data/constants';
|
||||
import CardHeader from '@src/course-outline/card-header/CardHeader';
|
||||
import SortableItem from '@src/course-outline/drag-helper/SortableItem';
|
||||
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
|
||||
@@ -39,7 +39,7 @@ interface SectionCardProps {
|
||||
onOpenPublishModal: () => void,
|
||||
onOpenConfigureModal: () => void,
|
||||
onEditSectionSubmit: (itemId: string, sectionId: string, displayName: string) => void,
|
||||
savingStatus: string,
|
||||
savingStatus?: RequestStatusType,
|
||||
onOpenDeleteModal: () => void,
|
||||
onOpenUnlinkModal: () => void,
|
||||
onDuplicateSubmit: () => void,
|
||||
@@ -301,7 +301,7 @@ const SectionCard = ({
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
savingStatus={savingStatus}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
|
||||
@@ -115,7 +115,6 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
|
||||
onNewUnitSubmit={jest.fn()}
|
||||
onAddUnitFromLibrary={handleOnAddUnitFromLibrary}
|
||||
isCustomRelativeDatesActive={false}
|
||||
savingStatus=""
|
||||
onEditSubmit={onEditSubectionSubmit}
|
||||
onDuplicateSubmit={jest.fn()}
|
||||
onOpenConfigureModal={jest.fn()}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { isEmpty } from 'lodash';
|
||||
|
||||
import CourseOutlineSubsectionCardExtraActionsSlot from '@src/plugin-slots/CourseOutlineSubsectionCardExtraActionsSlot';
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '@src/course-outline/data/slice';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import { RequestStatus, RequestStatusType } from '@src/data/constants';
|
||||
import CardHeader from '@src/course-outline/card-header/CardHeader';
|
||||
import SortableItem from '@src/course-outline/drag-helper/SortableItem';
|
||||
import { DragContext } from '@src/course-outline/drag-helper/DragContextProvider';
|
||||
@@ -40,7 +40,7 @@ interface SubsectionCardProps {
|
||||
isCustomRelativeDatesActive: boolean,
|
||||
onOpenPublishModal: () => void,
|
||||
onEditSubmit: (itemId: string, sectionId: string, displayName: string) => void,
|
||||
savingStatus: string,
|
||||
savingStatus?: RequestStatusType,
|
||||
onOpenDeleteModal: () => void,
|
||||
onOpenUnlinkModal: () => void,
|
||||
onDuplicateSubmit: () => void,
|
||||
@@ -303,7 +303,7 @@ const SubsectionCard = ({
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
savingStatus={savingStatus}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
|
||||
@@ -81,7 +81,6 @@ const renderComponent = (props?: object) => render(
|
||||
onOpenDeleteModal={jest.fn()}
|
||||
onOpenUnlinkModal={jest.fn()}
|
||||
onOpenConfigureModal={jest.fn()}
|
||||
savingStatus=""
|
||||
onEditSubmit={jest.fn()}
|
||||
onDuplicateSubmit={jest.fn()}
|
||||
getTitleLink={(id) => `/some/${id}`}
|
||||
|
||||
@@ -13,8 +13,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import CourseOutlineUnitCardExtraActionsSlot from '@src/plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '@src/course-outline/data/slice';
|
||||
import { fetchCourseSectionQuery } from '@src/course-outline/data/thunk';
|
||||
import { RequestStatus } from '@src/data/constants';
|
||||
import { isUnitReadOnly } from '@src/course-unit/data/utils';
|
||||
import { RequestStatus, RequestStatusType } from '@src/data/constants';
|
||||
import CardHeader from '@src/course-outline/card-header/CardHeader';
|
||||
import SortableItem from '@src/course-outline/drag-helper/SortableItem';
|
||||
import TitleLink from '@src/course-outline/card-header/TitleLink';
|
||||
@@ -33,7 +32,7 @@ interface UnitCardProps {
|
||||
onOpenPublishModal: () => void;
|
||||
onOpenConfigureModal: () => void;
|
||||
onEditSubmit: (itemId: string, sectionId: string, displayName: string) => void,
|
||||
savingStatus: string;
|
||||
savingStatus?: RequestStatusType;
|
||||
onOpenDeleteModal: () => void;
|
||||
onOpenUnlinkModal: () => void;
|
||||
onDuplicateSubmit: () => void;
|
||||
@@ -108,8 +107,6 @@ const UnitCard = ({
|
||||
};
|
||||
}, [upstreamInfo]);
|
||||
|
||||
const readOnly = isUnitReadOnly(unit);
|
||||
|
||||
// re-create actions object for customizations
|
||||
const actions = { ...unitActions };
|
||||
// add actions to control display of move up & down menu buton.
|
||||
@@ -247,7 +244,7 @@ const UnitCard = ({
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={readOnly || savingStatus === RequestStatus.IN_PROGRESS}
|
||||
savingStatus={savingStatus}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
|
||||
@@ -2353,14 +2353,14 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();
|
||||
|
||||
// Disable the "Edit" button
|
||||
// Edit button should be enabled even for library imported units
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
const editButton = within(unitHeaderTitle).getByRole(
|
||||
'button',
|
||||
{ name: 'Edit' },
|
||||
);
|
||||
expect(editButton).toBeInTheDocument();
|
||||
expect(editButton).toBeDisabled();
|
||||
expect(editButton).toBeEnabled();
|
||||
|
||||
// The "Publish" button should still be enabled
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
@@ -2371,14 +2371,6 @@ describe('<CourseUnit />', () => {
|
||||
expect(publishButton).toBeInTheDocument();
|
||||
expect(publishButton).toBeEnabled();
|
||||
|
||||
// Disable the "Manage Tags" button
|
||||
const manageTagsButton = screen.getByRole(
|
||||
'button',
|
||||
{ name: tagsDrawerMessages.manageTagsButton.defaultMessage },
|
||||
);
|
||||
expect(manageTagsButton).toBeInTheDocument();
|
||||
expect(manageTagsButton).toBeDisabled();
|
||||
|
||||
// Does not render the "Add Components" section
|
||||
expect(screen.queryByText(addComponentMessages.title.defaultMessage)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { PUBLISH_TYPES } from '../constants';
|
||||
import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
|
||||
import { isUnitImportedFromLib, normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
|
||||
|
||||
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function getVerticalData(unitId) {
|
||||
.get(getCourseSectionVerticalApiUrl(unitId));
|
||||
|
||||
const courseSectionVerticalData = normalizeCourseSectionVerticalData(data);
|
||||
courseSectionVerticalData.xblockInfo.readOnly = isUnitReadOnly(courseSectionVerticalData.xblockInfo);
|
||||
courseSectionVerticalData.xblockInfo.readOnly = isUnitImportedFromLib(courseSectionVerticalData.xblockInfo);
|
||||
|
||||
return courseSectionVerticalData;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export const updateXBlockBlockIdToId = (data: object): object => {
|
||||
* @param unit - uses the 'upstreamInfo' object if found.
|
||||
* @returns True if readOnly, False if editable.
|
||||
*/
|
||||
export const isUnitReadOnly = ({ upstreamInfo }: XBlock): boolean => (
|
||||
export const isUnitImportedFromLib = ({ upstreamInfo }: XBlock): boolean => (
|
||||
!!upstreamInfo
|
||||
&& !!upstreamInfo.upstreamRef
|
||||
&& upstreamInfo.upstreamRef.startsWith('lct:')
|
||||
|
||||
@@ -34,8 +34,6 @@ const HeaderTitle = ({
|
||||
COURSE_BLOCK_NAMES.component.id,
|
||||
].includes(currentItemData.category);
|
||||
|
||||
const readOnly = !!currentItemData.readOnly;
|
||||
|
||||
const onConfigureSubmit = (...arg) => {
|
||||
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
|
||||
};
|
||||
@@ -82,7 +80,6 @@ const HeaderTitle = ({
|
||||
className="ml-1 flex-shrink-0"
|
||||
iconAs={EditIcon}
|
||||
onClick={handleTitleEdit}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.altButtonSettings)}
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('<HeaderTitle />', () => {
|
||||
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('Units sourced from upstream show a disabled edit button', async () => {
|
||||
it('Units sourced from upstream show a enabled edit button', async () => {
|
||||
// Override mock unit with one sourced from an upstream library
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
@@ -95,7 +95,7 @@ describe('<HeaderTitle />', () => {
|
||||
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeDisabled();
|
||||
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeEnabled();
|
||||
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled();
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const messages = defineMessages({
|
||||
description: 'Button to save changes to the OLX',
|
||||
},
|
||||
advancedDetailsOLXCancelButton: {
|
||||
id: 'course-authoring.library-authoring.component.advanced.olx-save',
|
||||
id: 'course-authoring.library-authoring.component.advanced.olx-cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Button to cancel changes to the OLX',
|
||||
},
|
||||
|
||||
@@ -41,11 +41,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Remove from collection',
|
||||
description: 'Menu item for remove a component from collection.',
|
||||
},
|
||||
menuRemoveFromContainer: {
|
||||
id: 'course-authoring.library-authoring.component.menu.remove',
|
||||
defaultMessage: 'Remove from {containerType}',
|
||||
description: 'Menu item for remove an item from {containerType}.',
|
||||
},
|
||||
removeComponentFromCollectionSuccess: {
|
||||
id: 'course-authoring.library-authoring.component.remove-from-collection-success',
|
||||
defaultMessage: 'Item successfully removed',
|
||||
|
||||
@@ -122,7 +122,7 @@ const messages = defineMessages({
|
||||
description: 'Title text for the warning displayed before deleting a Subsection',
|
||||
},
|
||||
deleteSubsectionCourseMessaage: {
|
||||
id: 'course-authoring.library-authoring.subsection.delete-parent-message',
|
||||
id: 'course-authoring.library-authoring.subsection.delete-course-message',
|
||||
defaultMessage: 'This subsection is used {courseCount, plural, one {{courseCountText} time} other {{courseCountText} times}} in courses, and will stop receiving updates there.',
|
||||
description: 'Course usage details shown before deleting a subsection',
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@ const messages = defineMessages({
|
||||
description: 'Message when the library changes are reverted successfully.',
|
||||
},
|
||||
revertErrorMsg: {
|
||||
id: 'course-authoring.library-authoring.publish.error',
|
||||
id: 'course-authoring.library-authoring.revert.error',
|
||||
defaultMessage: 'There was an error reverting changes in the library.',
|
||||
description: 'Message when there is an error when reverting changes in the library.',
|
||||
},
|
||||
|
||||
@@ -71,11 +71,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Coming soon!',
|
||||
description: 'Temp placeholder for the collections container. This will be replaced with the actual collection list.',
|
||||
},
|
||||
createLibrary: {
|
||||
id: 'course-authoring.library-authoring.create-library',
|
||||
defaultMessage: 'Create library',
|
||||
description: 'Header for the create library form',
|
||||
},
|
||||
createLibraryTempPlaceholder: {
|
||||
id: 'course-authoring.library-authoring.create-library-temp-placeholder',
|
||||
defaultMessage: 'This is a placeholder for the create library form. This will be replaced with the actual form.',
|
||||
|
||||
Reference in New Issue
Block a user