Skip to content

Commit 955e7ba

Browse files
committed
feat: add container collections support
1 parent 04faf54 commit 955e7ba

File tree

23 files changed

+368
-185
lines changed

23 files changed

+368
-185
lines changed

src/course-unit/add-component/AddComponent.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ const AddComponent = ({
195195
>
196196
<ComponentPicker
197197
showOnlyPublished
198+
extraFilter={['NOT block_type = "unit"']}
198199
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
199200
onComponentSelected={handleLibraryV2Selection}
200201
onChangeComponentSelection={setSelectedComponents}

src/index.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,14 @@ const App = () => {
6666
<Route path="/libraries-v1" element={<StudioHome />} />
6767
<Route path="/library/create" element={<CreateLibrary />} />
6868
<Route path="/library/:libraryId/*" element={<LibraryLayout />} />
69-
<Route path="/component-picker" element={<ComponentPicker />} />
70-
<Route path="/component-picker/multiple" element={<ComponentPicker componentPickerMode="multiple" />} />
69+
<Route
70+
path="/component-picker"
71+
element={<ComponentPicker extraFilter={['NOT block_type = "unit"']} />}
72+
/>
73+
<Route
74+
path="/component-picker/multiple"
75+
element={<ComponentPicker componentPickerMode="multiple" extraFilter={['NOT block_type = "unit"']} />}
76+
/>
7177
<Route path="/legacy/preview-changes/:usageKey" element={<PreviewChangesEmbed />} />
7278
<Route path="/course/:courseId/*" element={<CourseAuthoringRoutes />} />
7379
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />

src/library-authoring/LibraryAuthoringPage.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
141141
libraryData,
142142
isLoadingLibraryData,
143143
showOnlyPublished,
144+
extraFilter: contextExtraFilter,
144145
componentId,
145146
collectionId,
146147
unitId,
@@ -223,6 +224,10 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
223224
extraFilter.push('last_published IS NOT NULL');
224225
}
225226

227+
if (contextExtraFilter) {
228+
extraFilter.push(...contextExtraFilter);
229+
}
230+
226231
const activeTypeFilters = {
227232
components: 'type = "library_block"',
228233
collections: 'type = "collection"',

src/library-authoring/collections/LibraryCollectionPage.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ const LibraryCollectionPage = () => {
109109
}
110110

111111
const { componentPickerMode } = useComponentPickerContext();
112-
const { showOnlyPublished, setCollectionId, componentId } = useLibraryContext();
112+
const {
113+
showOnlyPublished, extraFilter: contextExtraFilter, setCollectionId, componentId,
114+
} = useLibraryContext();
113115
const { sidebarComponentInfo, openInfoSidebar } = useSidebarContext();
114116

115117
const {
@@ -182,6 +184,10 @@ const LibraryCollectionPage = () => {
182184
extraFilter.push('last_published IS NOT NULL');
183185
}
184186

187+
if (contextExtraFilter) {
188+
extraFilter.push(...contextExtraFilter);
189+
}
190+
185191
return (
186192
<div className="d-flex">
187193
<div className="flex-grow-1">

src/library-authoring/common/context/ComponentPickerContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88

99
export interface SelectedComponent {
1010
usageKey: string;
11-
blockType: string;
11+
blockType?: string;
1212
}
1313

1414
export type ComponentSelectedEvent = (selectedComponent: SelectedComponent) => void;

src/library-authoring/common/context/LibraryContext.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export type LibraryContextData = {
3434
setUnitId: (unitId?: string) => void;
3535
// Only show published components
3636
showOnlyPublished: boolean;
37+
// Additional filtering
38+
extraFilter?: string[];
3739
// "Create New Collection" modal
3840
isCreateCollectionModalOpen: boolean;
3941
openCreateCollectionModal: () => void;
@@ -66,6 +68,7 @@ type LibraryProviderProps = {
6668
children?: React.ReactNode;
6769
libraryId: string;
6870
showOnlyPublished?: boolean;
71+
extraFilter?: string[]
6972
// If set, will initialize the current collection and/or component from the current URL
7073
skipUrlUpdate?: boolean;
7174

@@ -83,6 +86,7 @@ export const LibraryProvider = ({
8386
children,
8487
libraryId,
8588
showOnlyPublished = false,
89+
extraFilter = [],
8690
skipUrlUpdate = false,
8791
componentPicker,
8892
}: LibraryProviderProps) => {
@@ -139,6 +143,7 @@ export const LibraryProvider = ({
139143
readOnly,
140144
isLoadingLibraryData,
141145
showOnlyPublished,
146+
extraFilter,
142147
isCreateCollectionModalOpen,
143148
openCreateCollectionModal,
144149
closeCreateCollectionModal,
@@ -164,6 +169,7 @@ export const LibraryProvider = ({
164169
readOnly,
165170
isLoadingLibraryData,
166171
showOnlyPublished,
172+
extraFilter,
167173
isCreateCollectionModalOpen,
168174
openCreateCollectionModal,
169175
closeCreateCollectionModal,

src/library-authoring/component-info/ComponentManagement.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88

99
import { useLibraryContext } from '../common/context/LibraryContext';
1010
import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext';
11-
import { useLibraryBlockMetadata } from '../data/apiHooks';
11+
import { ContentTagsDrawer, useContentTaxonomyTagsData } from '../../content-tags-drawer';
12+
import { useLibraryBlockMetadata, useUpdateComponentCollections } from '../data/apiHooks';
1213
import StatusWidget from '../generic/status-widget';
14+
import { ManageCollections } from '../generic/manage-collections';
1315
import messages from './messages';
14-
import { ContentTagsDrawer, useContentTaxonomyTagsData } from '../../content-tags-drawer';
15-
import ManageCollections from './ManageCollections';
1616

1717
const ComponentManagement = () => {
1818
const intl = useIntl();
@@ -130,7 +130,11 @@ const ComponentManagement = () => {
130130
</Collapsible.Visible>
131131
</Collapsible.Trigger>
132132
<Collapsible.Body className="collapsible-body">
133-
<ManageCollections usageKey={usageKey} collections={componentMetadata.collections} />
133+
<ManageCollections
134+
opaqueKey={usageKey}
135+
collections={componentMetadata.collections}
136+
useUpdateCollectionsHook={useUpdateComponentCollections}
137+
/>
134138
</Collapsible.Body>
135139
</Collapsible.Advanced>
136140
</Stack>

src/library-authoring/component-info/messages.ts

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -136,51 +136,6 @@ const messages = defineMessages({
136136
defaultMessage: 'Component Preview',
137137
description: 'Title for preview modal',
138138
},
139-
manageCollectionsText: {
140-
id: 'course-authoring.library-authoring.component.manage-tab.collections.text',
141-
defaultMessage: 'Manage Collections',
142-
description: 'Header and button text for collection section in manage tab',
143-
},
144-
manageCollectionsAddBtnText: {
145-
id: 'course-authoring.library-authoring.component.manage-tab.collections.btn-text',
146-
defaultMessage: 'Add to Collection',
147-
description: 'Button text for collection section in manage tab',
148-
},
149-
manageCollectionsSearchPlaceholder: {
150-
id: 'course-authoring.library-authoring.component.manage-tab.collections.search-placeholder',
151-
defaultMessage: 'Search',
152-
description: 'Placeholder text for collection search in manage tab',
153-
},
154-
manageCollectionsSelectionLabel: {
155-
id: 'course-authoring.library-authoring.component.manage-tab.collections.selection-aria-label',
156-
defaultMessage: 'Collection selection',
157-
description: 'Aria label text for collection selection box',
158-
},
159-
manageCollectionsToComponentSuccess: {
160-
id: 'course-authoring.library-authoring.component.manage-tab.collections.add-success',
161-
defaultMessage: 'Component collections updated',
162-
description: 'Message to display on updating component collections',
163-
},
164-
manageCollectionsToComponentFailed: {
165-
id: 'course-authoring.library-authoring.component.manage-tab.collections.add-failed',
166-
defaultMessage: 'Failed to update Component collections',
167-
description: 'Message to display on failure of updating component collections',
168-
},
169-
manageCollectionsToComponentConfirmBtn: {
170-
id: 'course-authoring.library-authoring.component.manage-tab.collections.add-confirm-btn',
171-
defaultMessage: 'Confirm',
172-
description: 'Button text to confirm collections for a component',
173-
},
174-
manageCollectionsToComponentCancelBtn: {
175-
id: 'course-authoring.library-authoring.component.manage-tab.collections.add-cancel-btn',
176-
defaultMessage: 'Cancel',
177-
description: 'Button text to cancel collections selection for a component',
178-
},
179-
componentNotOrganizedIntoCollection: {
180-
id: 'course-authoring.library-authoring.component.manage-tab.collections.no-collections',
181-
defaultMessage: 'This component is not organized into any collection.',
182-
description: 'Message to display in manage collections section when component is not part of any collection.',
183-
},
184139
componentPickerSingleSelect: {
185140
id: 'course-authoring.library-authoring.component-picker.single-select',
186141
defaultMessage: 'Add to Course', // TODO: Change this message to a generic one?

src/library-authoring/component-picker/ComponentPicker.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const defaultSelectionChangedCallback: ComponentSelectionChangedEvent = (selecti
3838
window.parent.postMessage({ type: 'pickerSelectionChanged', selections }, '*');
3939
};
4040

41-
type ComponentPickerProps = { libraryId?: string, showOnlyPublished?: boolean } & (
41+
type ComponentPickerProps = { libraryId?: string, showOnlyPublished?: boolean, extraFilter?: string[] } & (
4242
{
4343
componentPickerMode?: 'single',
4444
onComponentSelected?: ComponentSelectedEvent,
@@ -54,6 +54,7 @@ export const ComponentPicker: React.FC<ComponentPickerProps> = ({
5454
/** Restrict the component picker to a specific library */
5555
libraryId,
5656
showOnlyPublished,
57+
extraFilter,
5758
componentPickerMode = 'single',
5859
/** This default callback is used to send the selected component back to the parent window,
5960
* when the component picker is used in an iframe.
@@ -105,6 +106,7 @@ export const ComponentPicker: React.FC<ComponentPickerProps> = ({
105106
<LibraryProvider
106107
libraryId={selectedLibrary}
107108
showOnlyPublished={calcShowOnlyPublished}
109+
extraFilter={extraFilter}
108110
skipUrlUpdate
109111
>
110112
<SidebarProvider>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
2+
import { Button } from '@openedx/paragon';
3+
import {
4+
AddCircleOutline,
5+
CheckBoxIcon,
6+
CheckBoxOutlineBlank,
7+
} from '@openedx/paragon/icons';
8+
9+
import { useComponentPickerContext } from '../common/context/ComponentPickerContext';
10+
import messages from './messages';
11+
12+
interface AddComponentWidgetProps {
13+
usageKey: string;
14+
blockType: string;
15+
}
16+
17+
const AddComponentWidget = ({ usageKey, blockType }: AddComponentWidgetProps) => {
18+
const intl = useIntl();
19+
20+
const {
21+
componentPickerMode,
22+
onComponentSelected,
23+
addComponentToSelectedComponents,
24+
removeComponentFromSelectedComponents,
25+
selectedComponents,
26+
} = useComponentPickerContext();
27+
28+
// istanbul ignore if: this should never happen
29+
if (!usageKey) {
30+
throw new Error('usageKey is required');
31+
}
32+
33+
// istanbul ignore if: this should never happen
34+
if (!componentPickerMode) {
35+
return null;
36+
}
37+
38+
if (componentPickerMode === 'single') {
39+
return (
40+
<Button
41+
variant="outline-primary"
42+
iconBefore={AddCircleOutline}
43+
onClick={() => {
44+
onComponentSelected({ usageKey, blockType });
45+
}}
46+
>
47+
<FormattedMessage {...messages.componentPickerSingleSelectTitle} />
48+
</Button>
49+
);
50+
}
51+
52+
if (componentPickerMode === 'multiple') {
53+
const isChecked = selectedComponents.some((component) => component.usageKey === usageKey);
54+
55+
const handleChange = () => {
56+
const selectedComponent = {
57+
usageKey,
58+
blockType,
59+
};
60+
if (!isChecked) {
61+
addComponentToSelectedComponents(selectedComponent);
62+
} else {
63+
removeComponentFromSelectedComponents(selectedComponent);
64+
}
65+
};
66+
67+
return (
68+
<Button
69+
variant="outline-primary"
70+
iconBefore={isChecked ? CheckBoxIcon : CheckBoxOutlineBlank}
71+
onClick={handleChange}
72+
>
73+
{intl.formatMessage(messages.componentPickerMultipleSelectTitle)}
74+
</Button>
75+
);
76+
}
77+
78+
// istanbul ignore next: this should never happen
79+
return null;
80+
};
81+
82+
export default AddComponentWidget;

0 commit comments

Comments
 (0)