Skip to content
20 changes: 14 additions & 6 deletions src/generic/block-type-utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
ContentPaste as ContentPasteIcon,
Edit as EditIcon,
EditNote as EditNoteIcon,
CalendarViewDay,
HelpOutline as HelpOutlineIcon,
LibraryAdd as LibraryIcon,
Lock as LockIcon,
Expand All @@ -15,6 +14,9 @@ import {
TextFields as TextFieldsIcon,
VideoCamera as VideoCameraIcon,
Folder,
ViewCarousel,
ViewDay,
WidthWide,
} from '@openedx/paragon/icons';
import NewsstandIcon from '../NewsstandIcon';

Expand All @@ -36,7 +38,9 @@ export const COMPONENT_TYPES = {
export const UNIT_TYPE_ICONS_MAP: Record<string, React.ComponentType> = {
video: VideoCameraIcon,
other: BookOpenIcon,
vertical: CalendarViewDay,
vertical: ViewDay,
sequential: WidthWide,
chapter: ViewCarousel,
problem: EditIcon,
lock: LockIcon,
};
Expand All @@ -57,8 +61,10 @@ export const COMPONENT_TYPE_ICON_MAP: Record<string, React.ComponentType> = {
export const STRUCTURAL_TYPE_ICONS: Record<string, React.ComponentType> = {
vertical: UNIT_TYPE_ICONS_MAP.vertical,
unit: UNIT_TYPE_ICONS_MAP.vertical,
sequential: Folder,
chapter: Folder,
sequential: UNIT_TYPE_ICONS_MAP.sequential,
subsection: UNIT_TYPE_ICONS_MAP.sequential,
chapter: UNIT_TYPE_ICONS_MAP.chapter,
section: UNIT_TYPE_ICONS_MAP.chapter,
collection: Folder,
libraryContent: Folder,
paste: ContentPasteIcon,
Expand All @@ -75,8 +81,10 @@ export const COMPONENT_TYPE_STYLE_COLOR_MAP = {
[COMPONENT_TYPES.dragAndDrop]: 'component-style-default',
vertical: 'component-style-vertical',
unit: 'component-style-vertical',
sequential: 'component-style-default',
chapter: 'component-style-default',
sequential: 'component-style-sequential',
subsection: 'component-style-sequential',
chapter: 'component-style-chapter',
section: 'component-style-chapter',
collection: 'component-style-collection',
other: 'component-style-other',
};
50 changes: 50 additions & 0 deletions src/generic/block-type-utils/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,56 @@
}
}

.component-style-sequential {
background-color: #EA3E3E;

.pgn__icon:not(.btn-icon-before) {
color: white;
}

.btn-icon {
&:hover, &:active, &:focus {
background-color: darken(#EA3E3E, 15%);
}
}

.btn {
background-color: lighten(#0B8E77, 10%);
border: 0;

&:hover, &:active, &:focus {
background-color: lighten(#0B8E77, 20%);
border: 1px solid $primary;
margin: -1px;
}
}
}

.component-style-chapter {
background-color: #45009E;

.pgn__icon:not(.btn-icon-before) {
color: white;
}

.btn-icon {
&:hover, &:active, &:focus {
background-color: darken(#45009E, 15%);
}
}

.btn {
background-color: lighten(#0B8E77, 10%);
border: 0;

&:hover, &:active, &:focus {
background-color: lighten(#0B8E77, 20%);
border: 1px solid $primary;
margin: -1px;
}
}
}

.component-style-other {
background-color: #646464;

Expand Down
16 changes: 10 additions & 6 deletions src/generic/key-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,19 @@ export const buildCollectionUsageKey = (learningContextKey: string, collectionId
};

export enum ContainerType {
Section = 'section',
Subsection = 'subsection',
Unit = 'unit',
/**
* Vertical is the old name for Unit. Generally, **please avoid using this term entirely in any libraries code** or
* anything based on the new Learning Core "Containers" framework - just call it a unit. We do still need to use this
* in the modulestore-based courseware, and currently the /xblock/ API used to copy library containers into courses
* also requires specifying this, though that should change to a better API that does the unit->vertical conversion
* automatically in the future.
* TODO: we should probably move this to a separate enum/mapping, and keep this for the new container types only.
* Chapter, Sequential and Vertical are the old names for section, subsection and unit.
* Generally, **please avoid using this term entirely in any libraries code** or
* anything based on the new Learning Core "Containers" framework - just call it a unit, section or subsection. We
* do still need to use this in the modulestore-based courseware, and currently the /xblock/ API used to copy
* library containers into courses also requires specifying this, though that should change to a better API
* that does the unit->vertical conversion automatically in the future.
*/
Chapter = 'chapter',
Sequential = 'sequential',
Vertical = 'vertical',
}

Expand Down
105 changes: 73 additions & 32 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -735,9 +735,22 @@ describe('<LibraryAuthoringPage />', () => {
expect(mockShowToast).toHaveBeenCalledWith('There is an error when creating the library collection');
});

it('should create a unit', async () => {
test.each([
{
label: 'should create a unit',
containerType: 'unit',
},
{
label: 'should create a section',
containerType: 'section',
},
{
label: 'should create a subsection',
containerType: 'subsection',
},
])('$label', async ({ containerType }) => {
await renderLibraryPage();
const title = 'This is a Test';
const title = `This is a Test ${containerType}`;
const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(200, {
id: '1',
Expand All @@ -753,22 +766,22 @@ describe('<LibraryAuthoringPage />', () => {
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New unit Modal
// Open New container Modal
const sidebar = screen.getByTestId('library-sidebar');
const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0];
fireEvent.click(newUnitButton);
const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i });
expect(unitModalHeading).toBeInTheDocument();
const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0];
fireEvent.click(newContainerButton);
const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') });
expect(containerModalHeading).toBeInTheDocument();

// Click on Cancel button
const cancelButton = screen.getByRole('button', { name: /cancel/i });
fireEvent.click(cancelButton);
expect(unitModalHeading).not.toBeInTheDocument();
expect(containerModalHeading).not.toBeInTheDocument();

// Open new unit modal again and create a unit
fireEvent.click(newUnitButton);
// Open new container modal again and create a container
fireEvent.click(newContainerButton);
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: /name your unit/i });
const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') });

fireEvent.change(nameField, { target: { value: title } });
fireEvent.click(createButton);
Expand All @@ -778,14 +791,27 @@ describe('<LibraryAuthoringPage />', () => {

expect(axiosMock.history.post[0].url).toBe(url);
expect(axiosMock.history.post[0].data).toContain(`"display_name":"${title}"`);
expect(axiosMock.history.post[0].data).toContain('"container_type":"unit"');
expect(mockShowToast).toHaveBeenCalledWith('Unit created successfully');
expect(axiosMock.history.post[0].data).toContain(`"container_type":"${containerType}"`);
expect(mockShowToast).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${containerType} created successfully`, 'i')));
});

it('should show validations in create unit', async () => {
test.each([
{
label: 'should show validations in create unit',
containerType: 'unit',
},
{
label: 'should show validations in create section',
containerType: 'section',
},
{
label: 'should show validations in create subsection',
containerType: 'subsection',
},
])('$label', async ({ containerType }) => {
await renderLibraryPage();

const title = 'This is a Test';
const title = `This is a Test ${containerType}`;
const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(200, {
id: '1',
Expand All @@ -801,27 +827,40 @@ describe('<LibraryAuthoringPage />', () => {
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New unit Modal
// Open New container Modal
const sidebar = screen.getByTestId('library-sidebar');
const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0];
fireEvent.click(newUnitButton);
const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i });
expect(unitModalHeading).toBeInTheDocument();
const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0];
fireEvent.click(newContainerButton);
const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') });
expect(containerModalHeading).toBeInTheDocument();

const nameField = screen.getByRole('textbox', { name: /name your unit/i });
const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') });
fireEvent.focus(nameField);
fireEvent.blur(nameField);

// Click on create with an empty name
const createButton = screen.getByRole('button', { name: /create/i });
fireEvent.click(createButton);

expect(await screen.findByText(/unit name is required/i)).toBeInTheDocument();
expect(await screen.findByText(new RegExp(`${containerType} name is required`, 'i'))).toBeInTheDocument();
});

it('should show error on create unit', async () => {
test.each([
{
label: 'should show error on create unit',
containerType: 'unit',
},
{
label: 'should show error on create section',
containerType: 'section',
},
{
label: 'should show error on create subsection',
containerType: 'subsection',
},
])('$label', async ({ containerType }) => {
await renderLibraryPage();
const displayName = 'This is a Test';
const displayName = `This is a Test ${containerType}`;
const url = getLibraryContainersApiUrl(mockContentLibrary.libraryId);
axiosMock.onPost(url).reply(500);

Expand All @@ -833,23 +872,25 @@ describe('<LibraryAuthoringPage />', () => {
fireEvent.click(newButton);
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New Unit Modal
// Open New container Modal
const sidebar = screen.getByTestId('library-sidebar');
const newUnitButton = within(sidebar).getAllByRole('button', { name: /unit/i })[0];
fireEvent.click(newUnitButton);
const unitModalHeading = await screen.findByRole('heading', { name: /new unit/i });
expect(unitModalHeading).toBeInTheDocument();
const newContainerButton = within(sidebar).getAllByRole('button', { name: new RegExp(containerType, 'i') })[0];
fireEvent.click(newContainerButton);
const containerModalHeading = await screen.findByRole('heading', { name: new RegExp(`new ${containerType}`, 'i') });
expect(containerModalHeading).toBeInTheDocument();

// Create a unit
// Create a container
const createButton = screen.getByRole('button', { name: /create/i });
const nameField = screen.getByRole('textbox', { name: /name your unit/i });
const nameField = screen.getByRole('textbox', { name: new RegExp(`name your ${containerType}`, 'i') });

fireEvent.change(nameField, { target: { value: displayName } });
fireEvent.click(createButton);

// Check error toast
await waitFor(() => expect(axiosMock.history.post.length).toBe(1));
expect(mockShowToast).toHaveBeenCalledWith('There is an error when creating the library unit');
expect(mockShowToast).toHaveBeenCalledWith(
expect.stringMatching(new RegExp(`There is an error when creating the library ${containerType}`, 'i')),
);
});

it('shows a single block when usageKey query param is set', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/library-authoring/LibraryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import LibraryAuthoringPage from './LibraryAuthoringPage';
import { LibraryProvider } from './common/context/LibraryContext';
import { SidebarProvider } from './common/context/SidebarContext';
import { CreateCollectionModal } from './create-collection';
import { CreateUnitModal } from './create-unit';
import { CreateContainerModal } from './create-container';
import LibraryCollectionPage from './collections/LibraryCollectionPage';
import { ComponentPicker } from './component-picker';
import { ComponentEditorModal } from './components/ComponentEditorModal';
Expand Down Expand Up @@ -50,7 +50,7 @@ const LibraryLayout = () => {
<>
{childPage}
<CreateCollectionModal />
<CreateUnitModal />
<CreateContainerModal />
<ComponentEditorModal />
</>
</SidebarProvider>
Expand Down
8 changes: 5 additions & 3 deletions src/library-authoring/add-content/AddContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const { libraryId } = mockContentLibrary;
const render = (collectionId?: string) => {
const params: { libraryId: string, collectionId?: string } = { libraryId, collectionId };
return baseRender(<AddContent />, {
path: '/library/:libraryId/:collectionId?',
path: '/library/:libraryId/collection/:collectionId?',
params,
extraWrapper: ({ children }) => (
<LibraryProvider
Expand All @@ -51,7 +51,7 @@ const render = (collectionId?: string) => {
const renderWithUnit = (unitId: string) => {
const params: { libraryId: string, unitId?: string } = { libraryId, unitId };
return baseRender(<AddContent />, {
path: '/library/:libraryId/:unitId?',
path: '/library/:libraryId/unit/:unitId?',
params,
extraWrapper: ({ children }) => (
<LibraryProvider
Expand Down Expand Up @@ -326,14 +326,16 @@ describe('<AddContent />', () => {
});
});

it('should not show collection/unit buttons when create component in container', async () => {
it('should not show collection, unit, section and subsection buttons when create component in unit', async () => {
const unitId = 'lct:orf1:lib1:unit:test-1';
renderWithUnit(unitId);

expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument();

expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Section' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Subsection' })).not.toBeInTheDocument();
});

it('should create a component in unit', async () => {
Expand Down
Loading