Skip to content

Commit 551b2b5

Browse files
committed
feat: add container collections support
1 parent 04faf54 commit 551b2b5

File tree

10 files changed

+191
-90
lines changed

10 files changed

+191
-90
lines changed

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/containers/ContainerOrganize.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,23 @@ import {
88
useToggle,
99
} from '@openedx/paragon';
1010
import {
11-
ExpandLess, ExpandMore, Tag,
11+
BookOpen,
12+
ExpandLess,
13+
ExpandMore,
14+
Tag,
1215
} from '@openedx/paragon/icons';
1316

1417
import { ContentTagsDrawer, useContentTaxonomyTagsData } from '../../content-tags-drawer';
18+
import { ManageCollections } from '../generic/manage-collections';
19+
import { useContainer, useUpdateContainerCollections } from '../data/apiHooks';
1520
import { useLibraryContext } from '../common/context/LibraryContext';
1621
import { useSidebarContext } from '../common/context/SidebarContext';
1722
import messages from './messages';
1823

1924
const ContainerOrganize = () => {
2025
const intl = useIntl();
2126
const [tagsCollapseIsOpen, , , toggleTags] = useToggle(true);
27+
const [collectionsCollapseIsOpen, , , toggleCollections] = useToggle(true);
2228

2329
const { readOnly } = useLibraryContext();
2430
const { sidebarComponentInfo } = useSidebarContext();
@@ -29,8 +35,10 @@ const ContainerOrganize = () => {
2935
throw new Error('containerId is required');
3036
}
3137

38+
const { data: containerMetadata } = useContainer(containerId);
3239
const { data: componentTags } = useContentTaxonomyTagsData(containerId);
3340

41+
const collectionsCount = useMemo(() => containerMetadata?.collections?.length || 0, [containerMetadata]);
3442
const tagsCount = useMemo(() => {
3543
if (!componentTags) {
3644
return 0;
@@ -50,6 +58,11 @@ const ContainerOrganize = () => {
5058
return result;
5159
}, [componentTags]);
5260

61+
// istanbul ignore if: this should never happen
62+
if (!containerMetadata) {
63+
return null;
64+
}
65+
5366
return (
5467
<Stack gap={3}>
5568
{[true, 'true'].includes(getConfig().ENABLE_TAGGING_TAXONOMY_PAGES)
@@ -82,6 +95,33 @@ const ContainerOrganize = () => {
8295
</Collapsible.Body>
8396
</Collapsible.Advanced>
8497
)}
98+
<Collapsible.Advanced
99+
open={collectionsCollapseIsOpen}
100+
className="collapsible-card border-0"
101+
>
102+
<Collapsible.Trigger
103+
onClick={toggleCollections}
104+
className="collapsible-trigger d-flex justify-content-between p-2"
105+
>
106+
<Stack gap={1} direction="horizontal">
107+
<Icon src={BookOpen} />
108+
{intl.formatMessage(messages.organizeTabCollectionsTitle, { count: collectionsCount })}
109+
</Stack>
110+
<Collapsible.Visible whenClosed>
111+
<Icon src={ExpandMore} />
112+
</Collapsible.Visible>
113+
<Collapsible.Visible whenOpen>
114+
<Icon src={ExpandLess} />
115+
</Collapsible.Visible>
116+
</Collapsible.Trigger>
117+
<Collapsible.Body className="collapsible-body">
118+
<ManageCollections
119+
opaqueKey={containerId}
120+
collections={containerMetadata.collections}
121+
useUpdateCollectionsHook={useUpdateContainerCollections}
122+
/>
123+
</Collapsible.Body>
124+
</Collapsible.Advanced>
85125
</Stack>
86126
);
87127
};

src/library-authoring/containers/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const messages = defineMessages({
2121
defaultMessage: 'Tags ({count})',
2222
description: 'Title for tags section in organize tab',
2323
},
24+
organizeTabCollectionsTitle: {
25+
id: 'course-authoring.library-authoring.container-sidebar.organize-tab.collections.title',
26+
defaultMessage: 'Collections ({count})',
27+
description: 'Title for collections section in organize tab',
28+
},
2429
settingsTabTitle: {
2530
id: 'course-authoring.library-authoring.container-sidebar.settings-tab.title',
2631
defaultMessage: 'Settings',

src/library-authoring/data/api.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const getLibraryBlockMetadataUrl = (usageKey: string) => `${getApiBaseUrl
4040
export const getLibraryBlockRestoreUrl = (usageKey: string) => `${getLibraryBlockMetadataUrl(usageKey)}restore/`;
4141

4242
/**
43-
* Get the URL for library block metadata.
43+
* Get the URL for library block collections.
4444
*/
4545
export const getLibraryBlockCollectionsUrl = (usageKey: string) => `${getLibraryBlockMetadataUrl(usageKey)}collections/`;
4646

@@ -115,6 +115,10 @@ export const getLibraryContainerApiUrl = (containerId: string) => `${getApiBaseU
115115
* Get the URL for a single container children api.
116116
*/
117117
export const getLibraryContainerChildrenApiUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}children/`;
118+
/**
119+
* Get the URL for library container collections.
120+
*/
121+
export const getLibraryContainerCollectionsUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}collections/`;
118122

119123
export interface ContentLibrary {
120124
id: string;
@@ -629,3 +633,12 @@ export async function getContainerChildren(containerId: string): Promise<Library
629633
const { data } = await client.get(getLibraryContainerChildrenApiUrl(containerId));
630634
return camelCaseObject(data);
631635
}
636+
637+
/**
638+
* Update container collections.
639+
*/
640+
export async function updateContainerCollections(containerId: string, collectionKeys: string[]) {
641+
await getAuthenticatedHttpClient().patch(getLibraryContainerCollectionsUrl(containerId), {
642+
collection_keys: collectionKeys,
643+
});
644+
}

src/library-authoring/data/apiHooks.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import {
5252
updateContainerMetadata,
5353
type UpdateContainerDataRequest,
5454
getContainerChildren,
55+
updateContainerCollections,
5556
} from './api';
5657
import { VersionSpec } from '../LibraryBlock';
5758

@@ -570,8 +571,9 @@ export const useRestoreCollection = (libraryId: string, collectionId: string) =>
570571
/**
571572
* Use this mutation to update collections related a component in a library
572573
*/
573-
export const useUpdateComponentCollections = (libraryId: string, usageKey: string) => {
574+
export const useUpdateComponentCollections = (usageKey: string) => {
574575
const queryClient = useQueryClient();
576+
const libraryId = getLibraryId(usageKey);
575577
return useMutation({
576578
mutationFn: async (collectionKeys: string[]) => updateComponentCollections(usageKey, collectionKeys),
577579
onSettled: () => {
@@ -631,3 +633,18 @@ export const useContainerChildren = (containerId: string) => (
631633
queryFn: () => getContainerChildren(containerId!),
632634
})
633635
);
636+
637+
/**
638+
* Use this mutation to update collections related a container in a library
639+
*/
640+
export const useUpdateContainerCollections = (containerId: string) => {
641+
const queryClient = useQueryClient();
642+
const libraryId = getLibraryId(containerId);
643+
return useMutation({
644+
mutationFn: async (collectionKeys: string[]) => updateContainerCollections(containerId, collectionKeys),
645+
onSettled: () => {
646+
queryClient.invalidateQueries({ queryKey: containerQueryKeys.container(containerId) });
647+
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
648+
},
649+
});
650+
};

src/library-authoring/component-info/ManageCollections.test.tsx renamed to src/library-authoring/generic/manage-collections/ManageCollections.test.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@ import fetchMock from 'fetch-mock-jest';
22

33
import userEvent from '@testing-library/user-event';
44
import MockAdapter from 'axios-mock-adapter/types';
5+
import { mockContentSearchConfig } from '../../../search-manager/data/api.mock';
56
import {
67
initializeMocks,
78
render as baseRender,
89
screen,
910
waitFor,
10-
} from '../../testUtils';
11-
import mockCollectionsResults from '../__mocks__/collection-search.json';
12-
import { mockContentSearchConfig } from '../../search-manager/data/api.mock';
13-
import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks';
11+
} from '../../../testUtils';
12+
import mockCollectionsResults from '../../__mocks__/collection-search.json';
13+
import { LibraryProvider } from '../../common/context/LibraryContext';
14+
import { SidebarProvider } from '../../common/context/SidebarContext';
15+
import { getLibraryBlockCollectionsUrl } from '../../data/api';
16+
import { useUpdateComponentCollections } from '../../data/apiHooks';
17+
import { mockContentLibrary, mockLibraryBlockMetadata } from '../../data/api.mocks';
1418
import ManageCollections from './ManageCollections';
15-
import { LibraryProvider } from '../common/context/LibraryContext';
16-
import { SidebarProvider } from '../common/context/SidebarContext';
17-
import { getLibraryBlockCollectionsUrl } from '../data/api';
1819

1920
let axiosMock: MockAdapter;
2021
let mockShowToast;
@@ -60,8 +61,9 @@ describe('<ManageCollections />', () => {
6061
const url = getLibraryBlockCollectionsUrl(mockLibraryBlockMetadata.usageKeyWithCollections);
6162
axiosMock.onPatch(url).reply(200);
6263
render(<ManageCollections
63-
usageKey={mockLibraryBlockMetadata.usageKeyWithCollections}
64+
opaqueKey={mockLibraryBlockMetadata.usageKeyWithCollections}
6465
collections={[{ title: 'My first collection', key: 'my-first-collection' }]}
66+
useUpdateCollectionsHook={useUpdateComponentCollections}
6567
/>);
6668
const manageBtn = await screen.findByRole('button', { name: 'Manage Collections' });
6769
userEvent.click(manageBtn);
@@ -73,10 +75,10 @@ describe('<ManageCollections />', () => {
7375
userEvent.click(confirmBtn);
7476
await waitFor(() => {
7577
expect(axiosMock.history.patch.length).toEqual(1);
76-
expect(mockShowToast).toHaveBeenCalledWith('Component collections updated');
77-
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
78-
collection_keys: ['my-first-collection', 'my-second-collection'],
79-
});
78+
});
79+
expect(mockShowToast).toHaveBeenCalledWith('Item collections updated');
80+
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
81+
collection_keys: ['my-first-collection', 'my-second-collection'],
8082
});
8183
expect(screen.queryByRole('search')).not.toBeInTheDocument();
8284
});
@@ -85,8 +87,9 @@ describe('<ManageCollections />', () => {
8587
const url = getLibraryBlockCollectionsUrl(mockLibraryBlockMetadata.usageKeyWithCollections);
8688
axiosMock.onPatch(url).reply(400);
8789
render(<ManageCollections
88-
usageKey={mockLibraryBlockMetadata.usageKeyWithCollections}
90+
opaqueKey={mockLibraryBlockMetadata.usageKeyWithCollections}
8991
collections={[]}
92+
useUpdateCollectionsHook={useUpdateComponentCollections}
9093
/>);
9194
screen.logTestingPlaygroundURL();
9295
const manageBtn = await screen.findByRole('button', { name: 'Add to Collection' });
@@ -99,20 +102,21 @@ describe('<ManageCollections />', () => {
99102
userEvent.click(confirmBtn);
100103
await waitFor(() => {
101104
expect(axiosMock.history.patch.length).toEqual(1);
102-
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
103-
collection_keys: ['my-second-collection'],
104-
});
105-
expect(mockShowToast).toHaveBeenCalledWith('Failed to update Component collections');
106105
});
106+
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({
107+
collection_keys: ['my-second-collection'],
108+
});
109+
expect(mockShowToast).toHaveBeenCalledWith('Failed to update item collections');
107110
expect(screen.queryByRole('search')).not.toBeInTheDocument();
108111
});
109112

110113
it('should close manage collections selection on cancel', async () => {
111114
const url = getLibraryBlockCollectionsUrl(mockLibraryBlockMetadata.usageKeyWithCollections);
112115
axiosMock.onPatch(url).reply(400);
113116
render(<ManageCollections
114-
usageKey={mockLibraryBlockMetadata.usageKeyWithCollections}
117+
opaqueKey={mockLibraryBlockMetadata.usageKeyWithCollections}
115118
collections={[]}
119+
useUpdateCollectionsHook={useUpdateComponentCollections}
116120
/>);
117121
const manageBtn = await screen.findByRole('button', { name: 'Add to Collection' });
118122
userEvent.click(manageBtn);
@@ -124,8 +128,8 @@ describe('<ManageCollections />', () => {
124128
userEvent.click(cancelBtn);
125129
await waitFor(() => {
126130
expect(axiosMock.history.patch.length).toEqual(0);
127-
expect(mockShowToast).not.toHaveBeenCalled();
128131
});
132+
expect(mockShowToast).not.toHaveBeenCalled();
129133
expect(screen.queryByRole('search')).not.toBeInTheDocument();
130134
});
131135
});

0 commit comments

Comments
 (0)