Skip to content

Commit 747f7b6

Browse files
authored
Add subsection/unit to section/subsection full-page view within library [FC-0090] (#2152)
* Removes the "content type" tab bar from the "Add Existing Component/Unit/Subsection" modal, and in all cases where only one type of content is shown for selection. * Updates the section/subsection sidebar to show "Existing Library Content" + "New Subsection" / "New Unit" buttons. * Updates the "Add New Unit/Subsection" buttons to directly launch the new container modal, instead of going via the container sidebar. * Ensures that whenever a subsection/unit is created from within a section/subsection, that it is linked to the parent section/subsection after created.
1 parent ebf4b7c commit 747f7b6

17 files changed

+486
-210
lines changed

src/library-authoring/LibraryAuthoringPage.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ const LibraryAuthoringPage = ({
158158
insideCollections,
159159
insideComponents,
160160
insideUnits,
161+
insideSection,
161162
insideSections,
163+
insideSubsection,
162164
insideSubsections,
163165
navigateTo,
164166
} = useLibraryRoutes();
@@ -251,8 +253,12 @@ const LibraryAuthoringPage = ({
251253
extraFilter.push(activeTypeFilters[activeKey]);
252254
}
253255

254-
// Disable filtering by block/problem type when viewing the Collections/Units/Sections/Subsections tab.
255-
const onlyOneType = (insideCollections || insideUnits || insideSections || insideSubsections);
256+
// Disable filtering by block/problem type when viewing the Collections/Units/Sections/Subsections tab,
257+
// or when inside a specific Section or Subsection.
258+
const onlyOneType = (
259+
insideCollections || insideUnits || insideSections || insideSubsections
260+
|| insideSection || insideSubsection
261+
);
256262
const overrideTypesFilter = onlyOneType
257263
? new TypesFilterData()
258264
: undefined;
@@ -298,14 +304,16 @@ const LibraryAuthoringPage = ({
298304
headerActions={<HeaderActions />}
299305
hideBorder
300306
/>
301-
<Tabs
302-
variant="tabs"
303-
activeKey={activeKey}
304-
onSelect={handleTabChange}
305-
className="my-3"
306-
>
307-
{visibleTabsToRender}
308-
</Tabs>
307+
{visibleTabs.length > 1 && (
308+
<Tabs
309+
variant="tabs"
310+
activeKey={activeKey}
311+
onSelect={handleTabChange}
312+
className="my-3"
313+
>
314+
{visibleTabsToRender}
315+
</Tabs>
316+
)}
309317
<ActionRow className="my-3">
310318
<SearchKeywordsField className="mr-3" />
311319
<FilterByTags />

src/library-authoring/add-content/AddContent.test.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ describe('<AddContent />', () => {
9090
expect(screen.queryByRole('button', { name: /video/i })).toBeInTheDocument();
9191
expect(screen.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument();
9292
expect(await screen.findByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument();
93+
expect(await screen.queryByRole('button', { name: /existing library content/i })).not.toBeInTheDocument();
9394
});
9495

9596
it('should render advanced content buttons', async () => {
@@ -331,6 +332,7 @@ describe('<AddContent />', () => {
331332
const unitId = 'lct:orf1:lib1:unit:test-1';
332333
renderWithContainer(unitId);
333334

335+
expect(await screen.findByRole('button', { name: /existing library content/i })).toBeInTheDocument();
334336
expect(await screen.findByRole('button', { name: 'Text' })).toBeInTheDocument();
335337

336338
expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument();
@@ -396,25 +398,32 @@ describe('<AddContent />', () => {
396398
expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this container.');
397399
});
398400

399-
it('should only show subsection button when inside a section', async () => {
401+
it('should only show subsection buttons when inside a section', async () => {
400402
mockClipboardEmpty.applyMock();
401403
const sectionId = 'lct:orf1:lib1:section:test-1';
402404
renderWithContainer(sectionId, 'section');
403405

404-
expect(await screen.findByRole('button', { name: 'Subsection' })).toBeInTheDocument();
406+
expect(await screen.findByRole('button', { name: /existing subsection/i })).toBeInTheDocument();
407+
408+
// Button is labeled "New Subsection" , not "Subsection"
409+
expect(await screen.findByRole('button', { name: /new subsection/i })).toBeInTheDocument();
410+
expect(screen.queryByRole('button', { name: 'Subsection' })).not.toBeInTheDocument();
405411

406412
expect(screen.queryByRole('button', { name: 'Collection' })).not.toBeInTheDocument();
407413
expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument();
408414
expect(screen.queryByRole('button', { name: 'Section' })).not.toBeInTheDocument();
409415
expect(screen.queryByRole('button', { name: 'Text' })).not.toBeInTheDocument();
410416
});
411417

412-
it('should only show unit button when inside a subsection', async () => {
418+
it('should only show unit buttons when inside a subsection', async () => {
413419
mockClipboardEmpty.applyMock();
414420
const subsectionId = 'lct:orf1:lib1:subsection:test-1';
415421
renderWithContainer(subsectionId, 'subsection');
416422

417-
expect(await screen.findByRole('button', { name: 'Unit' })).toBeInTheDocument();
423+
expect(await screen.findByRole('button', { name: /existing unit/i })).toBeInTheDocument();
424+
// Button is labeled "New Unit" in this context, not just "Unit"
425+
expect(await screen.findByRole('button', { name: /new unit/i })).toBeInTheDocument();
426+
expect(screen.queryByRole('button', { name: 'Unit' })).not.toBeInTheDocument();
418427

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

src/library-authoring/add-content/AddContent.tsx

Lines changed: 107 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ import { blockTypes } from '../../editors/data/constants/app';
3131

3232
import { useLibraryRoutes } from '../routes';
3333
import genericMessages from '../generic/messages';
34-
import messages from './messages';
34+
import { messages, getContentMessages } from './messages';
3535
import type { BlockTypeMetadata } from '../data/api';
3636
import { ContainerType } from '../../generic/key-utils';
3737

3838
type ContentType = {
3939
name: string,
40-
disabled: boolean,
40+
disabled?: boolean,
4141
icon?: React.ComponentType,
4242
blockType: string,
4343
};
@@ -64,7 +64,7 @@ type AddAdvancedContentViewProps = {
6464
const AddContentButton = ({ contentType, onCreateContent } : AddContentButtonProps) => {
6565
const {
6666
name,
67-
disabled,
67+
disabled = false,
6868
icon,
6969
blockType,
7070
} = contentType;
@@ -96,97 +96,132 @@ const AddContentView = ({
9696
insideSubsection,
9797
} = useLibraryRoutes();
9898

99-
const collectionButtonData = {
100-
name: intl.formatMessage(messages.collectionButton),
101-
disabled: false,
102-
blockType: 'collection',
103-
};
99+
const contentMessages = useMemo(() => (
100+
getContentMessages(insideSection, insideSubsection, insideUnit)
101+
), [insideSection, insideSubsection, insideUnit]);
102+
103+
const collectionButton = (
104+
<AddContentButton
105+
key="collection"
106+
contentType={{
107+
name: intl.formatMessage(contentMessages.collectionButton),
108+
blockType: 'collection',
109+
}}
110+
onCreateContent={onCreateContent}
111+
/>
112+
);
104113

105-
const unitButtonData = {
106-
name: intl.formatMessage(messages.unitButton),
107-
disabled: false,
108-
blockType: 'vertical',
109-
};
114+
const unitButton = (
115+
<AddContentButton
116+
key="unit"
117+
contentType={{
118+
name: intl.formatMessage(contentMessages.unitButton),
119+
blockType: 'unit',
120+
}}
121+
onCreateContent={onCreateContent}
122+
/>
123+
);
110124

111-
const sectionButtonData = {
112-
name: intl.formatMessage(messages.sectionButton),
113-
disabled: false,
114-
blockType: 'chapter',
115-
};
125+
const sectionButton = (
126+
<AddContentButton
127+
key="section"
128+
contentType={{
129+
name: intl.formatMessage(contentMessages.sectionButton),
130+
blockType: 'section',
131+
}}
132+
onCreateContent={onCreateContent}
133+
/>
134+
);
116135

117-
const subsectionButtonData = {
118-
name: intl.formatMessage(messages.subsectionButton),
119-
disabled: false,
120-
blockType: 'sequential',
121-
};
136+
const subsectionButton = (
137+
<AddContentButton
138+
key="subsection"
139+
contentType={{
140+
name: intl.formatMessage(contentMessages.subsectionButton),
141+
blockType: 'subsection',
142+
}}
143+
onCreateContent={onCreateContent}
144+
/>
145+
);
122146

123-
const libraryContentButtonData = {
124-
name: intl.formatMessage(messages.libraryContentButton),
125-
disabled: false,
126-
blockType: 'libraryContent',
127-
};
147+
const existingContentButton = (
148+
<AddContentButton
149+
key="libraryContent"
150+
contentType={{
151+
name: intl.formatMessage(contentMessages.libraryContentButton),
152+
blockType: 'libraryContent',
153+
}}
154+
onCreateContent={onCreateContent}
155+
/>
156+
);
128157

129-
/** List container content types that should be displayed based on current path */
130-
const visibleContentTypes = useMemo(() => {
158+
/* Note: for MVP we are hiding the unsupported types, not just disabling them. */
159+
const componentButtons = contentTypes.filter(ct => !ct.disabled).map((contentType) => (
160+
<AddContentButton
161+
key={`add-content-${contentType.blockType}`}
162+
contentType={contentType}
163+
onCreateContent={onCreateContent}
164+
/>
165+
));
166+
const separator = (
167+
<hr className="w-100 bg-gray-500" />
168+
);
169+
170+
/** List buttons that should be displayed based on current path */
171+
const visibleButtons = useMemo(() => {
131172
if (insideCollection) {
132-
// except for add collection button, show everthing.
173+
// except for add collection button, show everything.
133174
return [
134-
libraryContentButtonData,
135-
sectionButtonData,
136-
subsectionButtonData,
137-
unitButtonData,
175+
existingContentButton,
176+
sectionButton,
177+
subsectionButton,
178+
unitButton,
179+
separator,
180+
...componentButtons,
138181
];
139182
}
140183
if (insideUnit) {
141-
// Only show libraryContentButton
142-
return [libraryContentButtonData];
184+
// Only show existing content button + component buttons
185+
return [
186+
existingContentButton,
187+
separator,
188+
...componentButtons,
189+
];
143190
}
144191
if (insideSection) {
145-
// Should only allow adding subsections
146-
return [subsectionButtonData];
192+
// Only allow adding subsections
193+
return [
194+
existingContentButton,
195+
subsectionButton,
196+
];
147197
}
148198
if (insideSubsection) {
149-
// Should only allow adding units
150-
return [unitButtonData];
199+
// Only allow adding units
200+
return [
201+
existingContentButton,
202+
unitButton,
203+
];
151204
}
152-
// except for libraryContentButton, show everthing.
205+
// except for existing content, show everything.
153206
return [
154-
collectionButtonData,
155-
sectionButtonData,
156-
subsectionButtonData,
157-
unitButtonData,
207+
collectionButton,
208+
sectionButton,
209+
subsectionButton,
210+
unitButton,
211+
separator,
212+
...componentButtons,
158213
];
159-
}, [insideCollection, insideUnit, insideSection, insideSubsection]);
214+
}, [componentButtons, insideCollection, insideUnit, insideSection, insideSubsection]);
160215

161216
return (
162217
<>
163-
{visibleContentTypes.map((contentType) => (
164-
<AddContentButton
165-
key={contentType.blockType}
166-
contentType={contentType}
167-
onCreateContent={onCreateContent}
168-
/>
169-
))}
170-
{componentPicker && visibleContentTypes.includes(libraryContentButtonData) && (
171-
/// Show the "Add Library Content" button for units and collections
218+
{visibleButtons}
219+
{componentPicker && visibleButtons.includes(existingContentButton) && (
172220
<PickLibraryContentModal
173221
isOpen={isAddLibraryContentModalOpen}
174222
onClose={closeAddLibraryContentModal}
175223
/>
176224
)}
177-
{(!insideSection && !insideSubsection) && (
178-
<>
179-
<hr className="w-100 bg-gray-500" />
180-
{/* Note: for MVP we are hiding the unuspported types, not just disabling them. */}
181-
{contentTypes.filter(ct => !ct.disabled).map((contentType) => (
182-
<AddContentButton
183-
key={`add-content-${contentType.blockType}`}
184-
contentType={contentType}
185-
onCreateContent={onCreateContent}
186-
/>
187-
))}
188-
</>
189-
)}
190225
</>
191226
);
192227
};
@@ -423,9 +458,9 @@ const AddContent = () => {
423458
} else if (blockType === 'advancedXBlock') {
424459
showAdvancedList();
425460
} else if ([
426-
ContainerType.Vertical,
427-
ContainerType.Chapter,
428-
ContainerType.Sequential,
461+
ContainerType.Unit,
462+
ContainerType.Subsection,
463+
ContainerType.Section,
429464
].includes(blockType as ContainerType)) {
430465
setCreateContainerModalType(blockType as ContainerType);
431466
} else {

src/library-authoring/add-content/AddContentHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { FormattedMessage } from '@edx/frontend-platform/i18n';
3-
import messages from './messages';
3+
import { messages } from './messages';
44

55
const AddContentHeader = () => (
66
<span className="font-weight-bold m-1.5">

0 commit comments

Comments
 (0)