feat: course import filter in library and fix list order (#2773)

* Adds filter to show/hide previously imported courses in course import page in libraries.
* Fix issue with ordering of courses listing.
This commit is contained in:
Navin Karkera
2025-12-29 20:07:44 +05:30
committed by GitHub
parent 7f10575b52
commit 0f20267cc4
11 changed files with 394 additions and 94 deletions

View File

@@ -186,9 +186,17 @@ export const LibraryProvider = ({
);
};
export function useLibraryContext(): LibraryContextData {
export function useLibraryContext(
allowEmtpy?: false,
): LibraryContextData; // never undefined
export function useLibraryContext(
allowEmtpy: true,
): LibraryContextData | undefined; // may be undefined
export function useLibraryContext(
allowEmtpy?: boolean,
): LibraryContextData | undefined {
const ctx = useContext(LibraryContext);
if (ctx === undefined) {
if (!allowEmtpy && ctx === undefined) {
/* istanbul ignore next */
throw new Error('useLibraryContext() was used in a component without a <LibraryProvider> ancestor.');
}

View File

@@ -80,14 +80,27 @@ const renderComponent = (studioHomeState: Partial<StudioHomeState> = {}) => {
describe('<ImportStepperModal />', () => {
it('should render correctly', async () => {
const user = userEvent.setup();
renderComponent();
// Renders the stepper header
expect(await screen.findByText('Select Course')).toBeInTheDocument();
expect(await screen.findByText('Review Import Details')).toBeInTheDocument();
// Renders the course list and previously imported chip
expect(screen.getByText(/managing risk in the information age/i)).toBeInTheDocument();
expect(screen.getByText(/run 0/i)).toBeInTheDocument();
// Renders the course list and hides previously imported courses
expect(screen.queryByText(/run 0/i)).toBeInTheDocument(); // not imported before
// Hides previously imported courses.
expect(screen.queryByText(/managing risk in the information age/i)).not.toBeInTheDocument();
expect(screen.queryByText('Previously Imported')).not.toBeInTheDocument();
// use filter modal to show previously imported courses.
await user.click(await screen.findByRole('button', { name: 'Filter settings' }));
await user.click(await screen.findByRole('checkbox', { name: 'Show Previously Imported Courses' }));
await user.click(await screen.findByRole('button', { name: 'Save' }));
// Renders previously imported courses and badge
expect(await screen.findByText(/managing risk in the information age/i)).toBeInTheDocument();
expect(await screen.findByText(/run 0/i)).toBeInTheDocument();
expect(await screen.findByText('Previously Imported')).toBeInTheDocument();
// Renders cancel and next step buttons
@@ -116,6 +129,11 @@ describe('<ImportStepperModal />', () => {
description: 'This is a test course',
});
// use filter modal to show previously imported courses.
await user.click(await screen.findByRole('button', { name: 'Filter settings' }));
await user.click(await screen.findByRole('checkbox', { name: 'Show Previously Imported Courses' }));
await user.click(await screen.findByRole('button', { name: 'Save' }));
const nextButton = await screen.findByRole('button', { name: /next step/i });
expect(nextButton).toBeDisabled();
@@ -137,7 +155,7 @@ describe('<ImportStepperModal />', () => {
expect(await screen.findByText('Import Analysis in Progress')).toBeInTheDocument();
});
it('the course should remain selected on back', async () => {
it('the course should remain selected on back only for non-imported courses', async () => {
const user = userEvent.setup();
renderComponent();
@@ -145,7 +163,7 @@ describe('<ImportStepperModal />', () => {
expect(nextButton).toBeDisabled();
// Select a course
const courseCard = screen.getAllByRole('radio')[0];
const courseCard = (await screen.findAllByRole('radio'))[0];
await user.click(courseCard);
expect(courseCard).toBeChecked();
@@ -156,8 +174,53 @@ describe('<ImportStepperModal />', () => {
const backButton = await screen.findByRole('button', { name: /back/i });
await user.click(backButton);
expect(screen.getByText(/managing risk in the information age/i)).toBeInTheDocument();
expect(screen.getByText(/Run 0/i)).toBeInTheDocument();
expect(courseCard).toBeChecked();
expect(nextButton).toBeEnabled();
});
it('should hide previously imported courses on page change', async () => {
const user = userEvent.setup();
renderComponent();
// use filter modal to show previously imported courses.
await user.click(await screen.findByRole('button', { name: 'Filter settings' }));
await user.click(await screen.findByRole('checkbox', { name: 'Show Previously Imported Courses' }));
await user.click(await screen.findByRole('button', { name: 'Save' }));
const nextButton = await screen.findByRole('button', { name: /next step/i });
expect(nextButton).toBeDisabled();
// Select a course
const courseCard = (await screen.findAllByRole('radio'))[0];
await user.click(courseCard);
expect(courseCard).toBeChecked();
// Click next
expect(nextButton).toBeEnabled();
await user.click(nextButton);
const backButton = await screen.findByRole('button', { name: /back/i });
await user.click(backButton);
expect(screen.queryByText(/managing risk in the information age/i)).not.toBeInTheDocument();
expect((await screen.findAllByRole('radio'))[0]).not.toBeChecked();
// should be disabled
expect(await screen.findByRole('button', { name: /next step/i })).toBeDisabled();
});
it('should reset checkbox if modal is closed or cancelled without saving', async () => {
const user = userEvent.setup();
renderComponent();
// use filter modal to show previously imported courses.
await user.click(await screen.findByRole('button', { name: 'Filter settings' }));
await user.click(await screen.findByRole('checkbox', { name: 'Show Previously Imported Courses' }));
await user.click(await screen.findByRole('button', { name: 'Cancel' }));
await user.click(await screen.findByRole('button', { name: 'Filter settings' }));
const checkbox = await screen.findByRole('checkbox', { name: 'Show Previously Imported Courses' });
expect(checkbox).not.toBeChecked();
});
it('should import selected course on button click', async () => {
@@ -173,6 +236,12 @@ describe('<ImportStepperModal />', () => {
});
const user = userEvent.setup();
renderComponent();
// use filter modal to show previously imported courses.
await user.click(await screen.findByRole('button', { name: 'Filter settings' }));
await user.click(await screen.findByRole('checkbox', { name: 'Show Previously Imported Courses' }));
await user.click(await screen.findByRole('button', { name: 'Save' }));
axiosMock.onPost(bulkModulestoreMigrateUrl()).reply(200);
axiosMock.onGet(getCourseDetailsApiUrl('course-v1:HarvardX+123+2023')).reply(200, {
courseId: 'course-v1:HarvardX+123+2023',

View File

@@ -1,4 +1,4 @@
import { useContext, useMemo, useState } from 'react';
import { useContext, useState } from 'react';
import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
@@ -7,17 +7,19 @@ import {
} from '@openedx/paragon';
import { CoursesList, MigrationStatusProps } from '@src/studio-home/tabs-section/courses-tab';
import { useStudioHome } from '@src/studio-home/hooks';
import { useLibraryContext } from '@src/library-authoring/common/context/LibraryContext';
import Loading from '@src/generic/Loading';
import Header from '@src/header';
import SubHeader from '@src/generic/sub-header/SubHeader';
import { useMigrationInfo } from '@src/library-authoring/data/apiHooks';
import { useBulkModulestoreMigrate } from '@src/data/apiHooks';
import { ToastContext } from '@src/generic/toast-context';
import LoadingButton from '@src/generic/loading-button';
import { useCourseDetails } from '@src/course-outline/data/apiHooks';
import {
CourseImportFilterProvider,
useCourseImportFilter,
} from '@src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/context';
import { ReviewImportDetails } from './ReviewImportDetails';
import messages from '../messages';
import { HelpSidebar } from '../HelpSidebar';
@@ -26,31 +28,11 @@ type MigrationStep = 'select-course' | 'review-details';
export const MigrationStatus = ({
courseId,
allVisibleCourseIds,
}: MigrationStatusProps) => {
const { libraryId } = useLibraryContext();
const { processedMigrationInfo } = useCourseImportFilter() || {};
const {
data: migrationInfoData,
} = useMigrationInfo(allVisibleCourseIds);
const processedMigrationInfo = useMemo(() => {
const result = {};
if (migrationInfoData) {
for (const libraries of Object.values(migrationInfoData)) {
// The map key in `migrationInfoData` is in camelCase.
// In the processed map, we use the key in its original form.
if (libraries.length !== 0) {
result[libraries[0].sourceKey] = libraries.map(item => item.targetKey);
}
}
}
return result;
}, [migrationInfoData]);
const isPreviouslyMigrated = (
courseId in processedMigrationInfo && processedMigrationInfo[courseId].includes(libraryId)
);
const isPreviouslyMigrated = processedMigrationInfo?.[courseId]?.includes(libraryId);
if (!isPreviouslyMigrated) {
return null;
@@ -58,7 +40,7 @@ export const MigrationStatus = ({
return (
<div
key={`${courseId}-${processedMigrationInfo[courseId].join('-')}`}
key={`${courseId}-${processedMigrationInfo?.[courseId].join('-')}`}
className="previously-migrated-chip"
>
<Chip>
@@ -72,7 +54,7 @@ export const ImportStepperPage = () => {
const intl = useIntl();
const navigate = useNavigate();
const [currentStep, setCurrentStep] = useState<MigrationStep>('select-course');
const [selectedCourseId, setSelectedCourseId] = useState<string>();
const [selectedCourseId, setSelectedCourseId] = useState<string>('');
const [analysisCompleted, setAnalysisCompleted] = useState<boolean>(false);
const { data: courseData } = useCourseDetails(selectedCourseId);
const { libraryId, libraryData, readOnly } = useLibraryContext();
@@ -81,11 +63,8 @@ export const ImportStepperPage = () => {
// TODO: Modify single migration API to allow create collection
const migrate = useBulkModulestoreMigrate();
// Load the courses list
// The loading state is handled in `CoursesList`
useStudioHome();
const handleImportCourse = async () => {
// istanbul ignore if: this can never happen, just for satisfying type checker.
if (!selectedCourseId) {
return;
}
@@ -142,11 +121,16 @@ export const ImportStepperPage = () => {
eventKey="select-course"
title={intl.formatMessage(messages.importCourseSelectCourseStep)}
>
<CoursesList
<CourseImportFilterProvider
selectedCourseId={selectedCourseId}
handleSelect={setSelectedCourseId}
cardMigrationStatusWidget={MigrationStatus}
/>
>
<CoursesList
selectedCourseId={selectedCourseId}
handleSelect={setSelectedCourseId}
cardMigrationStatusWidget={MigrationStatus}
/>
</CourseImportFilterProvider>
</Stepper.Step>
<Stepper.Step
eventKey="review-details"
@@ -167,7 +151,7 @@ export const ImportStepperPage = () => {
</Button>
<Button
onClick={() => setCurrentStep('review-details')}
disabled={selectedCourseId === undefined}
disabled={!selectedCourseId}
>
<FormattedMessage {...messages.importCourseNext} />
</Button>

View File

@@ -17,6 +17,7 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
studioHomeData: {},
studioHomeCoursesRequestParams: {
currentPage: 1,
pageSize: 10,
search: '',
order: 'display_name',
archivedOnly: undefined,
@@ -30,6 +31,7 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
...initialState,
studioHomeCoursesRequestParams: {
currentPage: 2,
pageSize: 10,
search: 'test',
order: 'display_name',
archivedOnly: true,
@@ -41,6 +43,7 @@ describe('updateStudioHomeCoursesCustomParams action', () => {
const payload = {
currentPage: 2,
pageSize: 10,
search: 'test',
order: 'display_name',
archivedOnly: true,

View File

@@ -6,6 +6,7 @@ import { RequestStatus, type RequestStatusType } from '@src/data/constants';
export interface Params {
currentPage: number;
pageSize?: number;
search?: string;
order?: string;
archivedOnly?: boolean;
@@ -16,6 +17,7 @@ export interface Params {
export const studioHomeCoursesRequestParamsDefault: Params = {
currentPage: 1,
pageSize: 10,
search: '',
order: 'display_name',
archivedOnly: undefined,

View File

@@ -49,26 +49,30 @@ const useStudioHome = () => {
}
const courseListQueryString = courseListQuery.size ? `?${courseListQuery.toString()}` : '';
useEffect(() => {
dispatch(fetchStudioHomeData(courseListQueryString));
dispatch(fetchStudioHomeData(courseListQueryString, false, studioHomeCoursesParams));
setShowNewCourseContainer(false);
}, [courseListQueryString]);
useEffect(() => {
const firstPage = 1;
dispatch(fetchStudioHomeData(courseListQueryString, false, { page: firstPage, order: 'display_name' }));
dispatch(fetchStudioHomeData(courseListQueryString, false, {
...studioHomeCoursesParams,
page: firstPage,
order: 'display_name',
}));
}, []);
useEffect(() => {
if (courseCreatorSavingStatus === RequestStatus.SUCCESSFUL) {
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: '' }));
dispatch(fetchStudioHomeData());
dispatch(fetchStudioHomeData(undefined, false, studioHomeCoursesParams));
}
}, [courseCreatorSavingStatus]);
useEffect(() => {
if (deleteNotificationSavingStatus === RequestStatus.SUCCESSFUL) {
dispatch(updateSavingStatuses({ courseCreatorSavingStatus: '' }));
dispatch(fetchStudioHomeData());
dispatch(fetchStudioHomeData(undefined, false, studioHomeCoursesParams));
} else if (deleteNotificationSavingStatus === RequestStatus.FAILED) {
dispatch(updateSavingStatuses({ deleteNotificationSavingStatus: '' }));
}

View File

@@ -0,0 +1,109 @@
import {
createContext, ReactNode, useContext, useEffect, useMemo, useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useMigrationInfo } from '@src/library-authoring/data/apiHooks';
import { getLoadingStatuses, getStudioHomeCoursesParams, getStudioHomeData } from '@src/studio-home/data/selectors';
import { useLibraryContext } from '@src/library-authoring/common/context/LibraryContext';
import { fetchStudioHomeData } from '@src/studio-home/data/thunks';
import { updateStudioHomeCoursesCustomParams } from '@src/studio-home/data/slice';
import { RequestStatus } from '@src/data/constants';
export interface CourseImportFilterContextType {
processedMigrationInfo: Record<string, string[]>;
hidePreviouslyImportedCourses: boolean;
setHidePreviouslyImportedCourses: React.Dispatch<React.SetStateAction<boolean>>;
filteredCourses: object[];
}
export const CourseImportFilterContext = createContext<CourseImportFilterContextType | undefined>(undefined);
interface Props {
children: ReactNode;
handleSelect?: (courseId: string) => void;
selectedCourseId?: string;
}
const PAGE_SIZE = 500;
export const CourseImportFilterProvider = ({ handleSelect, selectedCourseId, children }: Props) => {
const { libraryId } = useLibraryContext(true) || {};
const dispatch = useDispatch();
const [hidePreviouslyImportedCourses, setHidePreviouslyImportedCourses] = useState(true);
const { courses, numPages } = useSelector(getStudioHomeData);
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
const { currentPage } = studioHomeCoursesParams;
const { courseLoadingStatus } = useSelector(getLoadingStatuses);
const isCourseListLoading = courseLoadingStatus === RequestStatus.IN_PROGRESS;
const allVisibleCourseIds = courses?.map(item => item.courseKey) || [];
const {
data: migrationInfoData,
isPending,
} = useMigrationInfo(allVisibleCourseIds, allVisibleCourseIds.length > 0);
const processedMigrationInfo: Record<string, string[]> = useMemo(() => {
const result = {};
if (migrationInfoData) {
for (const libraries of Object.values(migrationInfoData)) {
// The map key in `migrationInfoData` is in camelCase.
// In the processed map, we use the key in its original form.
if (libraries.length !== 0) {
result[libraries[0]?.sourceKey] = libraries.map(item => item.targetKey);
}
}
}
return result;
}, [migrationInfoData]);
const filteredCourses: any[] = useMemo(() => (hidePreviouslyImportedCourses && libraryId
? courses?.filter(course => !processedMigrationInfo[course.courseKey]?.includes(libraryId))
: courses), [hidePreviouslyImportedCourses, libraryId, processedMigrationInfo, courses]);
useEffect(() => {
// Filter the courses based on selected course id and handle the selection change
if (!isCourseListLoading && !isPending
&& filteredCourses.findIndex(course => course.courseKey === selectedCourseId) === -1) {
handleSelect?.('');
}
}, [filteredCourses, selectedCourseId, handleSelect]);
useEffect(() => {
// Fetch all studio home data for initial load to avoid pagingation
// This is required to avoid cases where we have very less number of non-imported courses per page
dispatch(updateStudioHomeCoursesCustomParams({ ...studioHomeCoursesParams, pageSize: PAGE_SIZE }));
dispatch(fetchStudioHomeData(undefined, false, { ...studioHomeCoursesParams, page: 1, pageSize: PAGE_SIZE }));
}, []);
// istanbul ignore next
useEffect(() => {
// HACK: If there are no courses that were not imported in the current page, then we need to fetch
// the next page of courses.
// FIXME: This workaround causes page flicker when the next page has also has no courses that were not imported.
if ((numPages > currentPage) && filteredCourses.length === 0 && !isCourseListLoading) {
dispatch(fetchStudioHomeData(undefined, false, { ...studioHomeCoursesParams, page: currentPage + 1 }));
dispatch(updateStudioHomeCoursesCustomParams({ ...studioHomeCoursesParams, currentPage: currentPage + 1 }));
}
}, [
numPages,
filteredCourses,
courses,
dispatch,
courseLoadingStatus,
studioHomeCoursesParams,
]);
const value = useMemo(() => ({
processedMigrationInfo,
hidePreviouslyImportedCourses,
setHidePreviouslyImportedCourses,
filteredCourses,
}), [processedMigrationInfo, hidePreviouslyImportedCourses, setHidePreviouslyImportedCourses]);
return (
<CourseImportFilterContext.Provider value={value}>
{children}
</CourseImportFilterContext.Provider>
);
};
export const useCourseImportFilter = () => useContext(CourseImportFilterContext);

View File

@@ -0,0 +1,95 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, Form, IconButton, ModalDialog,
} from '@openedx/paragon';
import { Settings } from '@openedx/paragon/icons';
import { useState } from 'react';
import { useCourseImportFilter } from './context';
import messages from './messages';
interface FilterModalProps {
show: boolean;
onClose: () => void;
handleSave: (isChecked: boolean) => void;
}
const FilterModal = ({ show, onClose, handleSave }: FilterModalProps) => {
const intl = useIntl();
const { hidePreviouslyImportedCourses } = useCourseImportFilter() || {};
const [isChecked, setChecked] = useState(!hidePreviouslyImportedCourses);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setChecked(e.target.checked);
const reset = () => setChecked(!hidePreviouslyImportedCourses);
const submit = () => {
handleSave(isChecked);
};
const cancel = () => {
onClose();
reset();
};
if (!show) {
return null;
}
return (
<ModalDialog
title={intl.formatMessage(messages.modalTitle)}
onClose={cancel}
isOverflowVisible={false}
hasCloseButton
isFullscreenOnMobile
isOpen={show}
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages.modalTitle)}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<Form.Checkbox checked={isChecked} onChange={handleChange}>
{intl.formatMessage(messages.checkboxLabel)}
</Form.Checkbox>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.cancelBtn)}
</ModalDialog.CloseButton>
<Button onClick={submit}>
{intl.formatMessage(messages.saveBtn)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
export const CourseImportFilter = () => {
const intl = useIntl();
const [show, setShow] = useState(false);
const importContext = useCourseImportFilter();
const { setHidePreviouslyImportedCourses } = importContext || {};
const handleSave = (isChecked: boolean) => {
setHidePreviouslyImportedCourses?.(!isChecked);
setShow(false);
};
// istanbul ignore if
if (typeof importContext === 'undefined') {
return null;
}
return (
<>
<IconButton
src={Settings}
alt={intl.formatMessage(messages.actionBtnAltText)}
onClick={() => setShow((prev) => !prev)}
/>
<FilterModal handleSave={handleSave} show={show} onClose={() => setShow(false)} />
</>
);
};

View File

@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
actionBtnAltText: {
id: 'course-authoring.studio-home.courses.import-filter.alt.text',
defaultMessage: 'Filter settings',
description: 'Alt text for settings icon to filter course list in course import in library.',
},
modalTitle: {
id: 'course-authoring.studio-home.courses.import-filter.modal.title',
defaultMessage: 'Show Imported Courses',
description: 'Title text of modal that allows users to see previously imported courses.',
},
checkboxLabel: {
id: 'course-authoring.studio-home.courses.import-filter.checkbox.label',
defaultMessage: 'Show Previously Imported Courses',
description: 'Label text of checkbox that allows users to see previously imported courses.',
},
cancelBtn: {
id: 'course-authoring.studio-home.courses.import-filter.cancelBtn.text',
defaultMessage: 'Cancel',
description: 'Cancel button text of modal that allows users to see previously imported courses.',
},
saveBtn: {
id: 'course-authoring.studio-home.courses.import-filter.saveBtn.text',
defaultMessage: 'Save',
description: 'Save button text of modal that allows users to see previously imported courses.',
},
});
export default messages;

View File

@@ -1,30 +1,48 @@
import { useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { SearchField } from '@openedx/paragon';
import { debounce } from 'lodash';
import { useIntl } from '@edx/frontend-platform/i18n';
import type { Dispatch } from 'redux';
import { getStudioHomeCoursesParams } from '../../../data/selectors';
import { updateStudioHomeCoursesCustomParams } from '../../../data/slice';
import { fetchStudioHomeData } from '../../../data/thunks';
import { LoadingSpinner } from '../../../../generic/Loading';
import { getStudioHomeCoursesParams } from '@src/studio-home/data/selectors';
import { updateStudioHomeCoursesCustomParams } from '@src/studio-home/data/slice';
import { fetchStudioHomeData } from '@src/studio-home/data/thunks';
import { LoadingSpinner } from '@src/generic/Loading';
import CoursesTypesFilterMenu from './courses-types-filter-menu';
import CoursesOrderFilterMenu from './courses-order-filter-menu';
import './index.scss';
import messages from './messages';
import { CourseImportFilter } from './courses-imported-filter-modal';
interface BaseFilter {
currentPage: number;
search: string | undefined;
order: string | undefined;
isFiltered: boolean;
archivedOnly: boolean | undefined;
activeOnly: boolean | undefined;
cleanFilters: boolean;
}
/* regex to check if a string has only whitespace
example " "
*/
const regexOnlyWhiteSpaces = /^\s+$/;
interface Props {
dispatch: Dispatch<any>,
locationValue: string,
onSubmitSearchField?: () => void,
isLoading?: boolean,
}
const CoursesFilters = ({
dispatch,
locationValue,
locationValue = '',
onSubmitSearchField,
isLoading,
}) => {
}: Props) => {
const studioHomeCoursesParams = useSelector(getStudioHomeCoursesParams);
const {
order,
@@ -37,7 +55,7 @@ const CoursesFilters = ({
const intl = useIntl();
const getFilterTypeData = (baseFilters) => ({
const getFilterTypeData = (baseFilters: BaseFilter) => ({
archivedCourses: { ...baseFilters, archivedOnly: true, activeOnly: undefined },
activeCourses: { ...baseFilters, activeOnly: true, archivedOnly: undefined },
allCourses: { ...baseFilters, archivedOnly: undefined, activeOnly: undefined },
@@ -47,8 +65,8 @@ const CoursesFilters = ({
oldestCourses: { ...baseFilters, order: 'created' },
});
const handleMenuFilterItemSelected = (filterType) => {
const baseFilters = {
const handleMenuFilterItemSelected = (filterType: string | number) => {
const baseFilters: BaseFilter = {
currentPage: 1,
search,
order,
@@ -73,7 +91,7 @@ const CoursesFilters = ({
dispatch(fetchStudioHomeData(locationValue, false, { page: 1, ...customParams }, true));
};
const handleSearchCourses = (searchValueDebounced) => {
const handleSearchCourses = (searchValueDebounced: string) => {
const valueFormatted = searchValueDebounced.trim();
const filterParams = {
search: valueFormatted.length > 0 ? valueFormatted : '',
@@ -122,21 +140,9 @@ const CoursesFilters = ({
<CoursesTypesFilterMenu onItemMenuSelected={handleMenuFilterItemSelected} />
<CoursesOrderFilterMenu onItemMenuSelected={handleMenuFilterItemSelected} />
<CourseImportFilter />
</div>
);
};
CoursesFilters.defaultProps = {
locationValue: '',
onSubmitSearchField: () => {},
isLoading: false,
};
CoursesFilters.propTypes = {
dispatch: PropTypes.func.isRequired,
locationValue: PropTypes.string,
onSubmitSearchField: PropTypes.func,
isLoading: PropTypes.bool,
};
export default CoursesFilters;

View File

@@ -26,10 +26,10 @@ import messages from '../messages';
import CoursesFilters from './courses-filters';
import ContactAdministrator from './contact-administrator';
import './index.scss';
import { useCourseImportFilter } from './courses-filters/courses-imported-filter-modal/context';
export interface MigrationStatusProps {
courseId: string;
allVisibleCourseIds: string[];
}
interface CardListProps {
@@ -62,10 +62,11 @@ const CardList = ({
migrationStatusWidget,
}: CardListProps) => {
const {
courses,
courses: allCourses,
numPages,
optimizationEnabled,
} = useSelector(getStudioHomeData);
const { filteredCourses: courses } = useCourseImportFilter() || { filteredCourses: allCourses };
const isNotFilteringCourses = !isFiltered && !isLoading;
const hasCourses = courses?.length > 0;
@@ -101,10 +102,7 @@ const CardList = ({
selectPosition={inSelectMode ? 'card' : undefined}
isSelected={inSelectMode && selectedCourseId === courseKey}
subtitleBeforeWidget={MigrationStatusWidget && (
<MigrationStatusWidget
courseId={courseKey}
allVisibleCourseIds={courses?.map(item => item.courseKey) || []}
/>
<MigrationStatusWidget courseId={courseKey} />
)}
/>
),
@@ -167,10 +165,11 @@ export const CoursesList: React.FC<Props> = ({
const intl = useIntl();
const location = useLocation();
const {
courses,
courses: allCourses,
coursesCount,
courseCreatorStatus,
} = useSelector(getStudioHomeData);
const { filteredCourses: courses } = useCourseImportFilter() || { filteredCourses: allCourses };
const {
courseLoadingStatus,
} = useSelector(getLoadingStatuses);
@@ -189,22 +188,12 @@ export const CoursesList: React.FC<Props> = ({
const inSelectMode = handleSelect !== undefined;
const handlePageSelected = (page) => {
const {
search,
order,
archivedOnly,
activeOnly,
} = studioHomeCoursesParams;
const customParams = {
search,
order,
archivedOnly,
activeOnly,
};
dispatch(fetchStudioHomeData(locationValue, false, { page, ...customParams }));
dispatch(updateStudioHomeCoursesCustomParams({ currentPage: page, isFiltered: true }));
dispatch(fetchStudioHomeData(locationValue, false, { ...studioHomeCoursesParams, page }));
dispatch(updateStudioHomeCoursesCustomParams({
...studioHomeCoursesParams,
currentPage: page,
isFiltered: true,
}));
};
const handleCleanFilters = () => {