Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c4511a4
feat: persistent component selection in library units page
navinkarkera Apr 28, 2025
ae2cb07
feat: unselect component on closing sidebar
navinkarkera Apr 28, 2025
50238b9
feat: enable component selection from whole div
navinkarkera Apr 28, 2025
eee0334
feat: scroll to newly created component in unit page
navinkarkera Apr 30, 2025
1b45028
feat: add 10px to library block iframes
navinkarkera Apr 30, 2025
4b9677c
feat: improve drag-n-drop for variable height components in unit page
navinkarkera May 1, 2025
5015780
fixup! feat: improve drag-n-drop for variable height components in un…
navinkarkera May 1, 2025
b189f76
fixup! feat: improve drag-n-drop for variable height components in un…
navinkarkera May 1, 2025
d340c5e
fix: add to collection menu item
navinkarkera May 1, 2025
4c88bb5
fix: remove condition block
navinkarkera May 4, 2025
82830d5
feat: fix add to container behaviour in all pages
navinkarkera May 4, 2025
22e5ee1
feat: show new manage tags section on clicking tag count in unit page
navinkarkera May 5, 2025
00bc2e4
feat: select newly created component in unit page
navinkarkera May 5, 2025
85f9717
refactor: vertically center draft icon
navinkarkera May 5, 2025
cba67e7
test: add tests
navinkarkera May 6, 2025
5d19c0d
chore: fix lint issues
navinkarkera May 6, 2025
d40975d
fix: failing tests
navinkarkera May 6, 2025
53664ac
test: fix coverage issues
navinkarkera May 6, 2025
3425f6d
fix: failing tests
navinkarkera May 6, 2025
b285cbc
chore: ignore sorting strategy from coverage
navinkarkera May 6, 2025
a896268
feat: drag-n-drop support for components with same ids in unit
navinkarkera May 6, 2025
49af64e
fix: failing tests
navinkarkera May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ coverage:
threshold: 0%
ignore:
- "src/grading-settings/grading-scale/react-ranger.js"
- "src/generic/DraggableList/verticalSortableList.ts"
- "src/index.js"
26 changes: 26 additions & 0 deletions src/content-tags-drawer/ContentTagsDrawer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const path = '/content/:contentId?/*';
const mockOnClose = jest.fn();
const mockSetBlockingSheet = jest.fn();
const mockNavigate = jest.fn();
const mockSidebarAction = jest.fn();
mockContentTaxonomyTagsData.applyMock();
mockTaxonomyListData.applyMock();
mockTaxonomyTagsData.applyMock();
Expand All @@ -40,6 +41,11 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));

jest.mock('../library-authoring/common/context/SidebarContext', () => ({
...jest.requireActual('../library-authoring/common/context/SidebarContext'),
useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }),
}));

const renderDrawer = (contentId, drawerParams = {}) => (
render(
<ContentTagsDrawerSheetContext.Provider value={drawerParams}>
Expand Down Expand Up @@ -184,6 +190,26 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});

it('should change to edit mode sidebar action is set to JumpToManageTags', async () => {
mockSidebarAction.mockReturnValueOnce('jump-to-manage-tags');
renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();

// Show delete tag buttons
expect(screen.getAllByRole('button', {
name: /delete/i,
}).length).toBe(2);

// Show add a tag select
expect(screen.getByText(/add a tag/i)).toBeInTheDocument();

// Show cancel button
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();

// Show save button
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});

it('should change to read mode when click on `Cancel` on drawer variant', async () => {
renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
Expand Down
12 changes: 10 additions & 2 deletions src/content-tags-drawer/ContentTagsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ContentTagsCollapsible from './ContentTagsCollapsible';
import Loading from '../generic/Loading';
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
import { SidebarActions, useSidebarContext } from '../library-authoring/common/context/SidebarContext';

interface TaxonomyListProps {
contentId: string;
Expand Down Expand Up @@ -244,6 +245,7 @@ const ContentTagsDrawer = ({
if (contentId === undefined) {
throw new Error('Error: contentId cannot be null.');
}
const { sidebarAction } = useSidebarContext();

const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
Expand All @@ -260,6 +262,7 @@ const ContentTagsDrawer = ({
closeToast,
setCollapsibleToInitalState,
otherTaxonomies,
toEditMode,
} = context;

let onCloseDrawer: () => void;
Expand Down Expand Up @@ -302,8 +305,13 @@ const ContentTagsDrawer = ({

// First call of the initial collapsible states
React.useEffect(() => {
setCollapsibleToInitalState();
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
// Open tag edit mode when sidebarAction is JumpToManageTags
if (sidebarAction === SidebarActions.JumpToManageTags) {
toEditMode();
} else {
setCollapsibleToInitalState();
}
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]);

const renderFooter = () => {
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
Expand Down
8 changes: 7 additions & 1 deletion src/content-tags-drawer/data/apiHooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import { useParams } from 'react-router';
import {
getTaxonomyTagsData,
getContentTaxonomyTagsData,
getContentData,
updateContentTaxonomyTags,
getContentTaxonomyTagsCount,
} from './api';
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { getLibraryId } from '../../generic/key-utils';

/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
Expand Down Expand Up @@ -129,6 +130,7 @@ export const useContentData = (contentId, enabled) => (
export const useContentTaxonomyTagsUpdater = (contentId) => {
const queryClient = useQueryClient();
const unitIframe = window.frames['xblock-iframe'];
const { unitId } = useParams();

return useMutation({
/**
Expand Down Expand Up @@ -158,6 +160,10 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
// Invalidate content search to update tags count
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
// If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again.
if (unitId) {
queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId));
}
}
},
onSuccess: /* istanbul ignore next */ () => {
Expand Down
2 changes: 1 addition & 1 deletion src/content-tags-drawer/data/apiHooks.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => {

const contentId = 'testerContent';
const taxonomyId = 123;
const mutation = useContentTaxonomyTagsUpdater(contentId);
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current;
const tagsData = [{
taxonomy: taxonomyId,
tags: ['tag1', 'tag2'],
Expand Down
2 changes: 1 addition & 1 deletion src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2230,7 +2230,7 @@ describe('<CourseOutline />', () => {
.reply(200, courseSectionMock);
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await userEvent.click(expandBtn);
userEvent.click(expandBtn);
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');

Expand Down
13 changes: 10 additions & 3 deletions src/generic/DraggableList/DraggableList.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useCallback } from 'react';
import { useCallback } from 'react';
import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';

import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
Expand All @@ -18,6 +17,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { verticalSortableListCollisionDetection } from './verticalSortableList';

const DraggableList = ({
itemList,
Expand Down Expand Up @@ -56,13 +56,20 @@ const DraggableList = ({
setActiveId?.(event.active.id);
}, [setActiveId]);

const handleDragCancel = useCallback(() => {
setActiveId?.(null);
}, [setActiveId]);

return (
<DndContext
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
collisionDetection={closestCenter}
collisionDetection={verticalSortableListCollisionDetection}
onDragStart={handleDragStart}
// autoScroll does not play well with verticalSortableListCollisionDetection strategy
autoScroll={false}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext
items={itemList}
Expand Down
3 changes: 2 additions & 1 deletion src/generic/DraggableList/SortableItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ const SortableItem = ({
};

return (
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
<div
ref={setNodeRef}
onClick={onClick}
>
<Card
style={style}
className="mx-0"
isClickable={isClickable}
onClick={onClick}
>
<ActionRow style={actionStyle}>
{actions}
Expand Down
80 changes: 80 additions & 0 deletions src/generic/DraggableList/verticalSortableList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* istanbul ignore file */
/**
This sorting strategy was copied over from https://github.com/clauderic/dnd-kit/pull/805
to resolve issues with variable sized draggables.
*/
import { CollisionDetection, DroppableContainer } from '@dnd-kit/core';
import { sortBy } from 'lodash';

const collision = (dropppableContainer?: DroppableContainer) => ({
id: dropppableContainer?.id ?? '',
value: dropppableContainer,
});

// Look for the first (/ furthest up / highest) droppable container that is at least
// 50% covered by the top edge of the dragging container.
const highestDroppableContainerMajorityCovered: CollisionDetection = ({
droppableContainers,
collisionRect,
}) => {
const ascendingDroppabaleContainers = sortBy(
droppableContainers,
(c) => c?.rect.current?.top,
);

for (const droppableContainer of ascendingDroppabaleContainers) {
const {
rect: { current: droppableRect },
} = droppableContainer;

if (droppableRect) {
const coveredPercentage = (droppableRect.top + droppableRect.height - collisionRect.top)
/ droppableRect.height;

if (coveredPercentage > 0.5) {
return [collision(droppableContainer)];
}
}
}

// if we haven't found anything then we are off the top, so return the first item
return [collision(ascendingDroppabaleContainers[0])];
};

// Look for the last (/ furthest down / lowest) droppable container that is at least
// 50% covered by the bottom edge of the dragging container.
const lowestDroppableContainerMajorityCovered: CollisionDetection = ({
droppableContainers,
collisionRect,
}) => {
const descendingDroppabaleContainers = sortBy(
droppableContainers,
(c) => c?.rect.current?.top,
).reverse();

for (const droppableContainer of descendingDroppabaleContainers) {
const {
rect: { current: droppableRect },
} = droppableContainer;

if (droppableRect) {
const coveredPercentage = (collisionRect.bottom - droppableRect.top) / droppableRect.height;

if (coveredPercentage > 0.5) {
return [collision(droppableContainer)];
}
}
}

// if we haven't found anything then we are off the bottom, so return the last item
return [collision(descendingDroppabaleContainers[0])];
};

export const verticalSortableListCollisionDetection: CollisionDetection = (
args,
) => {
if (args.collisionRect.top < (args.active.rect.current?.initial?.top ?? 0)) {
return highestDroppableContainerMajorityCovered(args);
}
return lowestDroppableContainerMajorityCovered(args);
};
3 changes: 2 additions & 1 deletion src/generic/hooks/tests/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ describe('useIframeBehavior', () => {
window.dispatchEvent(new MessageEvent('message', message));
});

expect(setIframeHeight).toHaveBeenCalledWith(500);
// +10 padding
expect(setIframeHeight).toHaveBeenCalledWith(510);
expect(setHasLoaded).toHaveBeenCalledWith(true);
});

Expand Down
3 changes: 2 additions & 1 deletion src/generic/hooks/useIframeBehavior.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const useIframeBehavior = ({

switch (type) {
case iframeMessageTypes.resize:
setIframeHeight(payload.height);
// Adding 10px as padding
setIframeHeight(payload.height + 10);
if (!hasLoaded && iframeHeight === 0 && payload.height > 0) {
setHasLoaded(true);
}
Expand Down
22 changes: 16 additions & 6 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const path = '/library/:libraryId/*';
const libraryTitle = mockContentLibrary.libraryData.title;

describe('<LibraryAuthoringPage />', () => {
beforeAll(() => {
jest.useFakeTimers();
});

beforeEach(async () => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
Expand All @@ -78,6 +82,10 @@ describe('<LibraryAuthoringPage />', () => {
});
});

afterAll(() => {
jest.useRealTimers();
});

const renderLibraryPage = async () => {
render(<LibraryLayout />, { path, params: { libraryId: mockContentLibrary.libraryId } });

Expand Down Expand Up @@ -392,7 +400,7 @@ describe('<LibraryAuthoringPage />', () => {
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});

it('should open component sidebar, showing manage tab on clicking add to collection menu item (component)', async () => {
it('should open component sidebar, showing manage tab on clicking add to collection menu item - component', async () => {
const mockResult0 = { ...mockResult }.results[0].hits[0];
const displayName = 'Introduction to Testing';
expect(mockResult0.display_name).toStrictEqual(displayName);
Expand All @@ -407,17 +415,18 @@ describe('<LibraryAuthoringPage />', () => {

const sidebar = screen.getByTestId('library-sidebar');

const { getByRole, queryByText } = within(sidebar);
const { getByRole, findByText } = within(sidebar);

await waitFor(() => expect(queryByText(displayName)).toBeInTheDocument());
expect(await findByText(displayName)).toBeInTheDocument();
jest.advanceTimersByTime(300);
expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage');
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);

await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});

it('should open component sidebar, showing manage tab on clicking add to collection menu item (unit)', async () => {
it('should open component sidebar, showing manage tab on clicking add to collection menu item - unit', async () => {
const displayName = 'Test Unit';
await renderLibraryPage();

Expand All @@ -430,9 +439,10 @@ describe('<LibraryAuthoringPage />', () => {

const sidebar = screen.getByTestId('library-sidebar');

const { getByRole, queryByText } = within(sidebar);
const { getByRole, findByText } = within(sidebar);

await waitFor(() => expect(queryByText(displayName)).toBeInTheDocument());
expect(await findByText(displayName)).toBeInTheDocument();
jest.advanceTimersByTime(300);
expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage');
const closeButton = getByRole('button', { name: /close/i });
fireEvent.click(closeButton);
Expand Down
Loading