diff --git a/src/components/PeopleManagement/CreateGroupModal.jsx b/src/components/PeopleManagement/CreateGroupModal.jsx
index ecf7e9aae3..212053809a 100644
--- a/src/components/PeopleManagement/CreateGroupModal.jsx
+++ b/src/components/PeopleManagement/CreateGroupModal.jsx
@@ -11,7 +11,7 @@ import {
import LmsApiService from '../../data/services/LmsApiService';
import SystemErrorAlertModal from '../learner-credit-management/cards/assignment-allocation-status-modals/SystemErrorAlertModal';
import CreateGroupModalContent from './CreateGroupModalContent';
-import { learnerCreditManagementQueryKeys } from '../learner-credit-management/data';
+import { peopleManagementQueryKeys } from './constants';
const CreateGroupModal = ({
isModalOpen,
@@ -53,7 +53,7 @@ const CreateGroupModal = ({
});
await LmsApiService.inviteEnterpriseLearnersToGroup(groupCreationResponse.data.uuid, requestBody);
queryClient.invalidateQueries({
- queryKey: learnerCreditManagementQueryKeys.group(enterpriseUUID),
+ queryKey: peopleManagementQueryKeys.group(enterpriseUUID),
});
setCreateButtonState('complete');
handleCloseCreateGroupModal();
diff --git a/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx b/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx
index ff2f93bd76..32b517baea 100644
--- a/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx
+++ b/src/components/PeopleManagement/EnterpriseCustomerUserDatatable.jsx
@@ -7,7 +7,7 @@ import {
} from '@openedx/paragon';
import { GROUP_MEMBERS_TABLE_DEFAULT_PAGE, GROUP_MEMBERS_TABLE_PAGE_SIZE } from './constants';
import MemberDetailsCell from './MemberDetailsCell';
-import AddMembersBulkAction from './AddMembersBulkAction';
+import AddMembersBulkAction from './GroupDetailPage/AddMembersBulkAction';
import RemoveMembersBulkAction from './RemoveMembersBulkAction';
import MemberJoinedDateCell from './MemberJoinedDateCell';
import { useEnterpriseMembersTableData } from './data/hooks';
diff --git a/src/components/PeopleManagement/AddMemberTableAction.jsx b/src/components/PeopleManagement/GroupDetailPage/AddMemberTableAction.jsx
similarity index 68%
rename from src/components/PeopleManagement/AddMemberTableAction.jsx
rename to src/components/PeopleManagement/GroupDetailPage/AddMemberTableAction.jsx
index ffbce51dd3..6e846598f0 100644
--- a/src/components/PeopleManagement/AddMemberTableAction.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/AddMemberTableAction.jsx
@@ -3,7 +3,13 @@ import { Add } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
const AddMemberTableAction = ({ openModal }) => (
-
+
);
AddMemberTableAction.propTypes = {
diff --git a/src/components/PeopleManagement/AddMembersBulkAction.jsx b/src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx
similarity index 94%
rename from src/components/PeopleManagement/AddMembersBulkAction.jsx
rename to src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx
index 33ed1e4f84..cd1d6a4d95 100644
--- a/src/components/PeopleManagement/AddMembersBulkAction.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/AddMembersBulkAction.jsx
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import { StatefulButton } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { useGetAllEnterpriseLearnerEmails } from './data/hooks/useEnterpriseLearnersTableData';
-import { getSelectedEmailsByRow } from './utils';
+import { useGetAllEnterpriseLearnerEmails } from '../data/hooks/useEnterpriseLearnersTableData';
+import { getSelectedEmailsByRow } from '../utils';
const AddMembersBulkAction = ({
isEntireTableSelected,
diff --git a/src/components/PeopleManagement/GroupDetailPage/DownloadCsvIconButton.jsx b/src/components/PeopleManagement/GroupDetailPage/DownloadCsvIconButton.jsx
new file mode 100644
index 0000000000..57bc5b7893
--- /dev/null
+++ b/src/components/PeopleManagement/GroupDetailPage/DownloadCsvIconButton.jsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, useIntl } from '@edx/frontend-platform/i18n';
+
+import {
+ Icon, IconButtonWithTooltip, Toast, useToggle,
+} from '@openedx/paragon';
+import { Download } from '@openedx/paragon/icons';
+import { logError } from '@edx/frontend-platform/logging';
+import GeneralErrorModal from '../GeneralErrorModal';
+import { downloadCsv, getTimeStampedFilename } from '../../../utils';
+
+const csvHeaders = ['Name', 'Email', 'Recent action', 'Enrollments'];
+
+const DownloadCsvIconButton = ({ fetchAllData, dataCount, testId }) => {
+ const [isToastOpen, openToast, closeToast] = useToggle(false);
+ const [isErrorModalOpen, openErrorModal, closeErrorModal] = useToggle(false);
+ const intl = useIntl();
+ const messages = defineMessages({
+ downloadToastText: {
+ id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.toast',
+ defaultMessage: 'Downloaded group members',
+ description: 'Toast message for the download button on the group detail page.',
+ },
+ downloadHoverText: {
+ id: 'adminPortal.peopleManagement.groupDetail.downloadCsv.hoverTooltip',
+ defaultMessage: `Download (${dataCount})`,
+ description: 'Tooltip message for the download button on the group detail page.',
+ },
+ });
+
+ const dataEntryToRow = (entry) => {
+ const { memberDetails: { userEmail, userName }, recentAction, enrollments } = entry;
+ return [userName, userEmail, recentAction, enrollments];
+ };
+
+ const handleClick = async () => {
+ fetchAllData().then((response) => {
+ const fileName = getTimeStampedFilename('group-report.csv');
+ downloadCsv(fileName, response.results, csvHeaders, dataEntryToRow);
+ openToast();
+ }).catch((err) => {
+ logError(err);
+ openErrorModal();
+ });
+ };
+
+ return (
+ <>
+ { isToastOpen
+ && (
+
+ {intl.formatMessage(messages.downloadToastText)}
+
+ )}
+
+
+ >
+ );
+};
+
+DownloadCsvIconButton.defaultProps = {
+ testId: 'download-csv-icon-button',
+};
+
+DownloadCsvIconButton.propTypes = {
+ fetchAllData: PropTypes.func.isRequired,
+ dataCount: PropTypes.number.isRequired,
+ testId: PropTypes.string,
+};
+
+export default DownloadCsvIconButton;
diff --git a/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx b/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
index 97449859a6..8da3181588 100644
--- a/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
+++ b/src/components/PeopleManagement/GroupDetailPage/GroupDetailPage.jsx
@@ -11,7 +11,7 @@ import { ROUTE_NAMES } from '../../EnterpriseApp/data/constants';
import DeleteGroupModal from './DeleteGroupModal';
import EditGroupNameModal from './EditGroupNameModal';
import formatDates from '../utils';
-import GroupMembersTable from '../GroupMembersTable';
+import GroupMembersTable from './GroupMembersTable';
import AddMembersModal from '../AddMembersModal/AddMembersModal';
const GroupDetailPage = () => {
@@ -27,6 +27,9 @@ const GroupDetailPage = () => {
isLoading: isTableLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData,
+ fetchAllEnterpriseGroupLearnersData,
+ refresh,
+ setRefresh,
} = useEnterpriseGroupLearnersTableData({ groupUuid, isAddMembersModalOpen });
const handleNameUpdate = (name) => {
setGroupName(name);
@@ -108,7 +111,6 @@ const GroupDetailPage = () => {
{
isLoading={isTableLoading}
tableData={enterpriseGroupLearnersTableData}
fetchTableData={fetchEnterpriseGroupLearnersTableData}
+ fetchAllData={fetchAllEnterpriseGroupLearnersData}
+ dataCount={enterpriseGroupLearnersTableData.itemCount}
groupUuid={groupUuid}
+ refresh={refresh}
+ setRefresh={setRefresh}
openAddMembersModal={openAddMembersModal}
/>
;
-const KabobMenu = () => (
-
-
-
-
-
-
-
-
-);
+
+
+
+
+
+
+
+ >
+ );
+};
+
+KabobMenu.propTypes = {
+ row: PropTypes.shape({}).isRequired,
+ groupUuid: PropTypes.string.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ setRefresh: PropTypes.func.isRequired,
+};
const selectColumn = {
id: 'selection',
@@ -49,7 +84,11 @@ const GroupMembersTable = ({
isLoading,
tableData,
fetchTableData,
+ fetchAllData,
+ dataCount,
groupUuid,
+ refresh,
+ setRefresh,
openAddMembersModal,
}) => {
const intl = useIntl();
@@ -69,9 +108,6 @@ const GroupMembersTable = ({
defaultColumnValues={{ Filter: TableTextFilter }}
FilterStatusComponent={FilterStatus}
numBreakoutFilters={2}
- tableActions={[
- ,
- ]}
columns={[
{
Header: intl.formatMessage({
@@ -106,9 +142,7 @@ const GroupMembersTable = ({
initialState={{
pageSize: GROUP_MEMBERS_TABLE_PAGE_SIZE,
pageIndex: GROUP_MEMBERS_TABLE_DEFAULT_PAGE,
- sortBy: [
- { id: 'memberDetails', desc: true },
- ],
+ sortBy: [{ id: 'memberDetails', desc: true }],
filters: [],
}}
additionalColumns={[
@@ -117,10 +151,23 @@ const GroupMembersTable = ({
Header: '',
// eslint-disable-next-line react/no-unstable-nested-components
Cell: (props) => (
-
+
),
},
]}
+ tableActions={[
+ ,
+ ,
+ ]}
fetchData={fetchTableData}
data={tableData.results}
itemCount={tableData.itemCount}
@@ -134,13 +181,16 @@ const GroupMembersTable = ({
GroupMembersTable.propTypes = {
isLoading: PropTypes.bool.isRequired,
tableData: PropTypes.shape({
- results: PropTypes.arrayOf(PropTypes.shape({
- })),
+ results: PropTypes.arrayOf(PropTypes.shape({})),
itemCount: PropTypes.number.isRequired,
pageCount: PropTypes.number.isRequired,
}).isRequired,
fetchTableData: PropTypes.func.isRequired,
+ fetchAllData: PropTypes.func.isRequired,
+ dataCount: PropTypes.number.isRequired,
groupUuid: PropTypes.string.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ setRefresh: PropTypes.func.isRequired,
openAddMembersModal: PropTypes.func.isRequired,
};
diff --git a/src/components/PeopleManagement/GroupDetailPage/RemoveMemberModal.jsx b/src/components/PeopleManagement/GroupDetailPage/RemoveMemberModal.jsx
new file mode 100644
index 0000000000..7ed1d09463
--- /dev/null
+++ b/src/components/PeopleManagement/GroupDetailPage/RemoveMemberModal.jsx
@@ -0,0 +1,88 @@
+import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
+import {
+ ActionRow, Button, ModalDialog,
+} from '@openedx/paragon';
+import { RemoveCircle } from '@openedx/paragon/icons';
+import { logError } from '@edx/frontend-platform/logging';
+
+import LmsApiService from '../../../data/services/LmsApiService';
+
+const RemoveMemberModal = ({
+ groupUuid, row, isOpen, close, openError, refresh, setRefresh,
+}) => {
+ const removeEnterpriseGroupMember = async () => {
+ try {
+ const rowEmail = row.id;
+ const formData = new FormData();
+ formData.append('learner_emails', rowEmail);
+ await LmsApiService.removeEnterpriseLearnersFromGroup(groupUuid, formData);
+ setRefresh(!refresh);
+ close();
+ } catch (error) {
+ close();
+ logError(error);
+ openError();
+ }
+ };
+ return (
+
+
+
+ Remove member?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Go back
+
+
+
+
+
+ );
+};
+
+RemoveMemberModal.propTypes = {
+ groupUuid: PropTypes.string.isRequired,
+ row: PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ }).isRequired,
+ isOpen: PropTypes.bool.isRequired,
+ close: PropTypes.func.isRequired,
+ openError: PropTypes.func.isRequired,
+ refresh: PropTypes.bool.isRequired,
+ setRefresh: PropTypes.func.isRequired,
+};
+
+export default RemoveMemberModal;
diff --git a/src/components/PeopleManagement/constants.js b/src/components/PeopleManagement/constants.js
index 5c9802d610..08fddb3637 100644
--- a/src/components/PeopleManagement/constants.js
+++ b/src/components/PeopleManagement/constants.js
@@ -12,7 +12,9 @@ export const GROUP_MEMBERS_TABLE_DEFAULT_PAGE = 0; // `DataTable` uses zero-inde
// Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories.
export const peopleManagementQueryKeys = {
all: ['people-management'],
+ group: (groupUuid) => [...peopleManagementQueryKeys.all, 'group', groupUuid],
members: (enterpriseUuid) => [...peopleManagementQueryKeys.all, 'members', enterpriseUuid],
+ removeMember: (groupUuid) => [...peopleManagementQueryKeys.all, 'removeMember', groupUuid],
};
export const MAX_INITIAL_LEARNER_EMAILS_DISPLAYED_COUNT = 15;
diff --git a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
index d3161ef3fd..d04805dc02 100644
--- a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupLearnersTableData.js
@@ -10,6 +10,7 @@ import LmsApiService from '../../../../data/services/LmsApiService';
const useEnterpriseGroupLearnersTableData = ({ groupUuid, isAddMembersModalOpen }) => {
const [isLoading, setIsLoading] = useState(true);
+ const [refresh, setRefresh] = useState(false);
const [enterpriseGroupLearnersTableData, setEnterpriseGroupLearnersTableData] = useState({
itemCount: 0,
pageCount: 0,
@@ -44,6 +45,7 @@ const useEnterpriseGroupLearnersTableData = ({ groupUuid, isAddMembersModalOpen
itemCount: data.count,
pageCount: data.numPages ?? Math.floor(data.count / options.pageSize),
results: data.results.filter(result => result.activatedAt),
+ options,
});
} catch (error) {
logError(error);
@@ -57,14 +59,24 @@ const useEnterpriseGroupLearnersTableData = ({ groupUuid, isAddMembersModalOpen
const debouncedFetchEnterpriseGroupLearnersData = useMemo(
() => debounce(fetchEnterpriseGroupLearnersData, 300),
- [fetchEnterpriseGroupLearnersData],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [fetchEnterpriseGroupLearnersData, refresh],
);
+ const fetchAllEnterpriseGroupLearnersData = useCallback(async () => {
+ const { options, itemCount } = enterpriseGroupLearnersTableData;
+ const fetchAllOptions = { ...options, page: 1, page_size: itemCount };
+ const response = await LmsApiService.fetchEnterpriseGroupLearners(groupUuid, fetchAllOptions);
+ return camelCaseObject(response.data);
+ }, [groupUuid, enterpriseGroupLearnersTableData]);
+
return {
isLoading,
enterpriseGroupLearnersTableData,
fetchEnterpriseGroupLearnersTableData: debouncedFetchEnterpriseGroupLearnersData,
+ fetchAllEnterpriseGroupLearnersData,
+ refresh,
+ setRefresh,
};
};
-
export default useEnterpriseGroupLearnersTableData;
diff --git a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js
index a8e97e495f..a7956c84e5 100644
--- a/src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js
+++ b/src/components/PeopleManagement/data/hooks/useEnterpriseGroupUuid.js
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { camelCaseObject } from '@edx/frontend-platform/utils';
-import { learnerCreditManagementQueryKeys } from '../../../learner-credit-management/data/constants';
import LmsApiService from '../../../../data/services/LmsApiService';
+import { peopleManagementQueryKeys } from '../../constants';
/**
* Retrieves an enterprise group by the group UUID from the API.
@@ -17,7 +17,7 @@ const getEnterpriseGroupUuid = async ({ groupUuid }) => {
};
const useEnterpriseGroupUuid = (groupUuid, { queryOptions } = {}) => useQuery({
- queryKey: learnerCreditManagementQueryKeys.group(groupUuid),
+ queryKey: peopleManagementQueryKeys.group(groupUuid),
queryFn: () => getEnterpriseGroupUuid({ groupUuid }),
...queryOptions,
});
diff --git a/src/components/PeopleManagement/tests/DownloadCsvIconButton.test.jsx b/src/components/PeopleManagement/tests/DownloadCsvIconButton.test.jsx
new file mode 100644
index 0000000000..f5fc01800d
--- /dev/null
+++ b/src/components/PeopleManagement/tests/DownloadCsvIconButton.test.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { logError } from '@edx/frontend-platform/logging';
+import {
+ act, fireEvent, render, screen, waitFor,
+} from '@testing-library/react';
+
+import '@testing-library/jest-dom/extend-expect';
+
+import userEvent from '@testing-library/user-event';
+import DownloadCsvIconButton from '../GroupDetailPage/DownloadCsvIconButton';
+import { downloadCsv } from '../../../utils';
+
+jest.mock('file-saver', () => ({
+ ...jest.requireActual('file-saver'),
+ saveAs: jest.fn(),
+}));
+
+jest.mock('../../../utils', () => ({
+ downloadCsv: jest.fn(),
+ getTimeStampedFilename: (suffix) => `2024-04-20-${suffix}`,
+}));
+
+jest.mock('@edx/frontend-platform/logging', () => ({
+ ...jest.requireActual('@edx/frontend-platform/logging'),
+ logError: jest.fn(),
+}));
+
+const mockData = {
+ results: [
+ {
+ memberDetails: {
+ userEmail: 'ga-linda@oz.com',
+ userName: 'Galinda Upland',
+ },
+ recentAction: 'Accepted: January 06, 2021',
+ enrollments: 0,
+ },
+ {
+ memberDetails: {
+ userEmail: 'elphaba@oz.com',
+ userName: 'Elphaba Throppe',
+ },
+ recentAction: 'Accepted: January 05, 2021',
+ enrollments: 3,
+ },
+ ],
+};
+
+const testId = 'test-id-1';
+const DEFAULT_PROPS = {
+ fetchAllData: jest.fn(() => Promise.resolve(mockData)),
+ dataCount: mockData.results.length,
+ testId,
+};
+
+const DownloadCsvIconButtonWrapper = props => (
+
+
+
+);
+
+describe('DownloadCsvIconButton', () => {
+ const flushPromises = () => new Promise(setImmediate);
+
+ it('renders download csv button correctly.', async () => {
+ render();
+ const downloadIcon = screen.getByTestId(testId);
+ expect(downloadIcon).toBeInTheDocument();
+
+ // show download tooltip
+ act(() => {
+ fireEvent.mouseOver(downloadIcon);
+ });
+ await waitFor(() => {
+ expect(screen.getByText('Download (2)')).toBeInTheDocument();
+ });
+
+ // Click the download button
+ screen.getByTestId(testId).click();
+ await flushPromises();
+
+ expect(DEFAULT_PROPS.fetchAllData).toHaveBeenCalled();
+ const expectedFileName = '2024-04-20-group-report.csv';
+ const expectedHeaders = ['Name', 'Email', 'Recent action', 'Enrollments'];
+ expect(downloadCsv).toHaveBeenCalledWith(expectedFileName, mockData.results, expectedHeaders, expect.any(Function));
+ });
+ it('download button should handle error returned by the API endpoint.', async () => {
+ const props = {
+ ...DEFAULT_PROPS,
+ fetchAllData: jest.fn(() => Promise.reject(new Error('Error fetching data'))),
+ };
+ render();
+ const downloadIcon = screen.getByTestId(testId);
+
+ expect(downloadIcon).toBeInTheDocument();
+
+ act(() => {
+ userEvent.click(downloadIcon);
+ });
+ await flushPromises();
+
+ expect(DEFAULT_PROPS.fetchAllData).toHaveBeenCalled();
+ expect(logError).toHaveBeenCalled();
+ });
+});
diff --git a/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx b/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
index 9d6c3bd052..2145898477 100644
--- a/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
+++ b/src/components/PeopleManagement/tests/GroupDetailPage.test.jsx
@@ -158,9 +158,17 @@ describe('', () => {
LmsApiService.removeEnterpriseGroup.mockResolvedValueOnce({ status: 204 });
render();
const deleteGroupIcon = screen.getByTestId('delete-group-icon');
+ // Open tooltip
+ expect(screen.queryByRole('tooltip')).toBeNull();
+ fireEvent.mouseOver(deleteGroupIcon);
+ await waitFor(() => {
+ expect(screen.queryByRole('tooltip')).not.toBeNull();
+ expect(screen.getAllByText('Delete group')).toHaveLength(1);
+ });
deleteGroupIcon.click();
- expect(screen.getByText('Delete group')).toBeInTheDocument();
+ // Open delete group modal
+ expect(screen.getByText('Delete group?')).toBeInTheDocument();
expect(screen.getByText('This action cannot be undone.'));
const deleteGroupButton = screen.getByTestId('delete-group-button');
deleteGroupButton.click();
diff --git a/src/components/learner-credit-management/data/constants.js b/src/components/learner-credit-management/data/constants.js
index 195b3e7b99..5e9d91abe6 100644
--- a/src/components/learner-credit-management/data/constants.js
+++ b/src/components/learner-credit-management/data/constants.js
@@ -121,10 +121,10 @@ export const learnerCreditManagementQueryKeys = {
budgetEnterpriseOffer: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'ecommerce'],
budgetActivity: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'activity'],
budgetActivityOverview: (budgetId) => [...learnerCreditManagementQueryKeys.budgetActivity(budgetId), 'overview'],
- group: (groupUuid) => [...learnerCreditManagementQueryKeys.all, 'group', groupUuid],
budgetGroupLearners: (budgetId) => [...learnerCreditManagementQueryKeys.budget(budgetId), 'group learners'],
enterpriseCustomer: (enterpriseId) => [...learnerCreditManagementQueryKeys.all, 'enterpriseCustomer', enterpriseId],
flexGroup: (enterpriseId) => [...learnerCreditManagementQueryKeys.enterpriseCustomer(enterpriseId), 'flexGroup'],
+ group: (groupUuid) => [...learnerCreditManagementQueryKeys.all, 'group', groupUuid],
catalog: (catalog) => [...learnerCreditManagementQueryKeys.all, 'catalog', catalog],
catalogContainsContentItem: (catalogUuid, contentKey) => [
...learnerCreditManagementQueryKeys.catalog(catalogUuid),