diff --git a/src/library-authoring/common/context/LibraryContext.tsx b/src/library-authoring/common/context/LibraryContext.tsx index cc3476c9f..7b775a279 100644 --- a/src/library-authoring/common/context/LibraryContext.tsx +++ b/src/library-authoring/common/context/LibraryContext.tsx @@ -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 ancestor.'); } diff --git a/src/library-authoring/import-course/stepper/ImportStepperPage.test.tsx b/src/library-authoring/import-course/stepper/ImportStepperPage.test.tsx index 5f9d986a1..023dab71e 100644 --- a/src/library-authoring/import-course/stepper/ImportStepperPage.test.tsx +++ b/src/library-authoring/import-course/stepper/ImportStepperPage.test.tsx @@ -80,14 +80,27 @@ const renderComponent = (studioHomeState: Partial = {}) => { describe('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { }); 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', diff --git a/src/library-authoring/import-course/stepper/ImportStepperPage.tsx b/src/library-authoring/import-course/stepper/ImportStepperPage.tsx index d7ecb1563..2e8cb55b2 100644 --- a/src/library-authoring/import-course/stepper/ImportStepperPage.tsx +++ b/src/library-authoring/import-course/stepper/ImportStepperPage.tsx @@ -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 (
@@ -72,7 +54,7 @@ export const ImportStepperPage = () => { const intl = useIntl(); const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState('select-course'); - const [selectedCourseId, setSelectedCourseId] = useState(); + const [selectedCourseId, setSelectedCourseId] = useState(''); const [analysisCompleted, setAnalysisCompleted] = useState(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)} > - + > + + { diff --git a/src/studio-home/data/slice.test.js b/src/studio-home/data/slice.test.js index 97e5446d3..c4de0ec66 100644 --- a/src/studio-home/data/slice.test.js +++ b/src/studio-home/data/slice.test.js @@ -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, diff --git a/src/studio-home/data/slice.ts b/src/studio-home/data/slice.ts index 8bed445b8..6c4b3b35f 100644 --- a/src/studio-home/data/slice.ts +++ b/src/studio-home/data/slice.ts @@ -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, diff --git a/src/studio-home/hooks.tsx b/src/studio-home/hooks.tsx index 64abeb3ed..bc5dc249f 100644 --- a/src/studio-home/hooks.tsx +++ b/src/studio-home/hooks.tsx @@ -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: '' })); } diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/context.tsx b/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/context.tsx new file mode 100644 index 000000000..2f67f77e6 --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/context.tsx @@ -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; + hidePreviouslyImportedCourses: boolean; + setHidePreviouslyImportedCourses: React.Dispatch>; + filteredCourses: object[]; +} + +export const CourseImportFilterContext = createContext(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 = 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 ( + + {children} + + ); +}; + +export const useCourseImportFilter = () => useContext(CourseImportFilterContext); diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/index.tsx b/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/index.tsx new file mode 100644 index 000000000..87581aa4e --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/index.tsx @@ -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) => setChecked(e.target.checked); + + const reset = () => setChecked(!hidePreviouslyImportedCourses); + + const submit = () => { + handleSave(isChecked); + }; + + const cancel = () => { + onClose(); + reset(); + }; + + if (!show) { + return null; + } + + return ( + + + + {intl.formatMessage(messages.modalTitle)} + + + + + {intl.formatMessage(messages.checkboxLabel)} + + + + + + {intl.formatMessage(messages.cancelBtn)} + + + + + + ); +}; + +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 ( + <> + setShow((prev) => !prev)} + /> + setShow(false)} /> + + ); +}; diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/messages.ts b/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/messages.ts new file mode 100644 index 000000000..db3405497 --- /dev/null +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/courses-imported-filter-modal/messages.ts @@ -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; diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx b/src/studio-home/tabs-section/courses-tab/courses-filters/index.tsx similarity index 77% rename from src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx rename to src/studio-home/tabs-section/courses-tab/courses-filters/index.tsx index d3d6873ea..00c2a3ef3 100644 --- a/src/studio-home/tabs-section/courses-tab/courses-filters/index.jsx +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/index.tsx @@ -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, + 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 = ({ +
); }; -CoursesFilters.defaultProps = { - locationValue: '', - onSubmitSearchField: () => {}, - isLoading: false, -}; - -CoursesFilters.propTypes = { - dispatch: PropTypes.func.isRequired, - locationValue: PropTypes.string, - onSubmitSearchField: PropTypes.func, - isLoading: PropTypes.bool, -}; - export default CoursesFilters; diff --git a/src/studio-home/tabs-section/courses-tab/index.tsx b/src/studio-home/tabs-section/courses-tab/index.tsx index d14c36c17..337a0e8d3 100644 --- a/src/studio-home/tabs-section/courses-tab/index.tsx +++ b/src/studio-home/tabs-section/courses-tab/index.tsx @@ -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 && ( - item.courseKey) || []} - /> + )} /> ), @@ -167,10 +165,11 @@ export const CoursesList: React.FC = ({ 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 = ({ 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 = () => {