diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index 84662e2ac3..6b8e968277 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -18,8 +18,6 @@ import Loading from './generic/Loading'; const CourseAuthoringPage = ({ courseId, children }) => { const dispatch = useDispatch(); - const STORE = useSelector(state => state); - console.log('STORE', STORE); useEffect(() => { dispatch(fetchCourseDetail(courseId)); diff --git a/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx b/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx index 6625333919..47c7bd7063 100644 --- a/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx +++ b/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx @@ -10,6 +10,7 @@ import { import { useSelector } from 'react-redux'; import { CheckCircle, RadioButtonUnchecked } from '@openedx/paragon/icons'; +import { getWaffleFlags } from '../../data/selectors'; import messages from './messages'; const ChecklistItemBody = ({ @@ -20,7 +21,7 @@ const ChecklistItemBody = ({ // injected intl, }) => { - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const navigate = useNavigate(); const handleClick = (e, url) => { diff --git a/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx b/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx index 61a87d60a8..98c49812e2 100644 --- a/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx +++ b/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx @@ -23,53 +23,36 @@ const defaultProps = { checkId: 'welcomeMessage', isCompleted: false, updateLink: 'https://example.com/update', - intl: { - formatMessage: jest.fn(({ defaultMessage }) => defaultMessage), - }, }; -const waffleFlags = { - ENABLE_NEW_COURSE_UPDATES_PAGE: false, +const renderComponent = (props = {}, mockWaffleFlags = { useNewUpdatesPage: false }) => { + useSelector.mockReturnValue(mockWaffleFlags); + return render( + + + , + ); }; describe('ChecklistItemBody', () => { - beforeEach(() => { - useSelector.mockReturnValue({ waffleFlags }); - }); - afterEach(() => { jest.clearAllMocks(); }); it('renders uncompleted icon when isCompleted is false', () => { - render( - - - , - ); - + renderComponent(); const uncompletedIcon = screen.getByTestId('uncompleted-icon'); expect(uncompletedIcon).toBeInTheDocument(); }); it('renders completed icon when isCompleted is true', () => { - render( - - - , - ); - + renderComponent({ isCompleted: true }); const completedIcon = screen.getByTestId('completed-icon'); expect(completedIcon).toBeInTheDocument(); }); it('renders short and long descriptions based on checkId', () => { - render( - - - , - ); - + renderComponent(); const shortDescription = screen.getByText(messages.welcomeMessageShortDescription.defaultMessage); const longDescription = screen.getByText(messages.welcomeMessageLongDescription.defaultMessage); @@ -78,38 +61,21 @@ describe('ChecklistItemBody', () => { }); it('renders update hyperlink when updateLink is provided', () => { - render( - - - , - ); - + renderComponent(); const updateLink = screen.getByTestId('update-hyperlink'); expect(updateLink).toBeInTheDocument(); }); - it('navigates to internal course page if ENABLE_NEW_COURSE_UPDATES_PAGE flag is enabled', () => { - useSelector.mockReturnValue({ waffleFlags: { ENABLE_NEW_COURSE_UPDATES_PAGE: true } }); - - render( - - - , - ); - + it('navigates to internal course page if useNewUpdatesPage flag is enabled', () => { + renderComponent({}, { useNewUpdatesPage: true }); const updateLink = screen.getByTestId('update-hyperlink'); fireEvent.click(updateLink); expect(mockNavigate).toHaveBeenCalledWith(`/course/${defaultProps.courseId}/course_info`); }); - it('redirects to external link if ENABLE_NEW_COURSE_UPDATES_PAGE flag is disabled', () => { - render( - - - , - ); - + it('redirects to external link if useNewUpdatesPage flag is disabled', () => { + renderComponent(); const updateLink = screen.getByTestId('update-hyperlink'); fireEvent.click(updateLink); diff --git a/src/course-outline/hooks.jsx b/src/course-outline/hooks.jsx index 0f1254d2cf..20baabfb6b 100644 --- a/src/course-outline/hooks.jsx +++ b/src/course-outline/hooks.jsx @@ -6,6 +6,7 @@ import { getConfig } from '@edx/frontend-platform'; import { copyToClipboard } from '../generic/data/thunks'; import { getSavingStatus as getGenericSavingStatus } from '../generic/data/selectors'; +import { getWaffleFlags } from '../data/selectors'; import { RequestStatus } from '../data/constants'; import { COURSE_BLOCK_NAMES } from './constants'; import { @@ -58,7 +59,7 @@ import { const useCourseOutline = ({ courseId }) => { const dispatch = useDispatch(); const navigate = useNavigate(); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const { reindexLink, diff --git a/src/course-outline/status-bar/StatusBar.jsx b/src/course-outline/status-bar/StatusBar.jsx index a2ad214b2c..cdfcd6b182 100644 --- a/src/course-outline/status-bar/StatusBar.jsx +++ b/src/course-outline/status-bar/StatusBar.jsx @@ -13,6 +13,7 @@ import { useSelector } from 'react-redux'; import { ContentTagsDrawerSheet } from '../../content-tags-drawer'; import TagCount from '../../generic/tag-count'; import { useHelpUrls } from '../../help-urls/hooks'; +import { getWaffleFlags } from '../../data/selectors'; import { VIDEO_SHARING_OPTIONS } from '../constants'; import { useContentTagsCount } from '../../generic/data/apiHooks'; import messages from './messages'; @@ -45,7 +46,7 @@ const StatusBar = ({ }) => { const intl = useIntl(); const { config } = useContext(AppContext); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const { courseReleaseDate, diff --git a/src/course-unit/breadcrumbs/Breadcrumbs.jsx b/src/course-unit/breadcrumbs/Breadcrumbs.jsx index 54e18bdabd..244006b807 100644 --- a/src/course-unit/breadcrumbs/Breadcrumbs.jsx +++ b/src/course-unit/breadcrumbs/Breadcrumbs.jsx @@ -8,6 +8,7 @@ import { } from '@openedx/paragon/icons'; import { getConfig } from '@edx/frontend-platform'; +import { getWaffleFlags } from '../../data/selectors'; import { createCorrectInternalRoute } from '../../utils'; import { getCourseSectionVertical } from '../data/selectors'; import messages from './messages'; @@ -17,7 +18,7 @@ const Breadcrumbs = () => { const { ancestorXblocks } = useSelector(getCourseSectionVertical); const [section, subsection] = ancestorXblocks ?? []; const navigate = useNavigate(); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const handleClick = (e, url) => { e.preventDefault(); @@ -44,7 +45,7 @@ const Breadcrumbs = () => { {section.children.map(({ url, displayName }) => ( handleClick(e, createCorrectInternalRoute(`${url}`))} + onClick={(e) => handleClick(e, createCorrectInternalRoute(url))} className="small" data-testid="breadcrumbs-section-dropdown-item" > @@ -73,7 +74,7 @@ const Breadcrumbs = () => { {subsection.children.map(({ url, displayName }) => ( handleClick(e, createCorrectInternalRoute(`${url}`))} + onClick={(e) => handleClick(e, createCorrectInternalRoute(url))} className="small" data-testid="breadcrumbs-subsection-dropdown-item" > diff --git a/src/data/api.js b/src/data/api.js index 22072ae160..718e206f50 100644 --- a/src/data/api.js +++ b/src/data/api.js @@ -17,27 +17,11 @@ export async function getCourseDetail(courseId, username) { } export async function getWaffleFlags(courseId) { - // const { data } = await getAuthenticatedHttpClient() - // .get(`${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/course_waffle_flags`); - - const data = { - use_new_home_page: true, - use_new_custom_pages: true, - use_new_schedule_details_page: true, - use_new_advanced_settings_page: true, - use_new_grading_page: true, - use_new_updates_page: true, - use_new_import_page: true, - use_new_export_page: true, - use_new_files_uploads_page: true, - use_new_video_uploads_page: true, - use_new_course_outline_page: true, - use_new_unit_page: true, - use_new_course_team_page: true, - use_new_certificates_page: true, - use_new_textbooks_page: true, - use_new_group_configurations_page: true, - }; + const apiUrl = courseId + ? `${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/course_waffle_flags/${courseId}/` + : `${getConfig().STUDIO_BASE_URL}/api/contentstore/v1/course_waffle_flags/`; + const { data } = await getAuthenticatedHttpClient() + .get(apiUrl); return normalizeCourseDetail(data); } diff --git a/src/data/selectors.js b/src/data/selectors.js new file mode 100644 index 0000000000..86076bcccb --- /dev/null +++ b/src/data/selectors.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const getWaffleFlags = (state) => state.courseDetail.waffleFlags; diff --git a/src/data/thunks.js b/src/data/thunks.js index 2ae6f426f2..2ce79bb3b5 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -37,7 +37,6 @@ export function fetchWaffleFlags(courseId) { try { const waffleFlags = await getWaffleFlags(courseId); dispatch(updateStatus({ courseId, status: RequestStatus.SUCCESSFUL })); - console.log('fetchWaffleFlags thunk', waffleFlags); dispatch(fetchWaffleFlagsSuccess({ waffleFlags })); } catch (error) { if (error.response && error.response.status === 404) { diff --git a/src/generic/help-sidebar/HelpSidebar.jsx b/src/generic/help-sidebar/HelpSidebar.jsx index 0017d0512d..cdcfde15b9 100644 --- a/src/generic/help-sidebar/HelpSidebar.jsx +++ b/src/generic/help-sidebar/HelpSidebar.jsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; -import { getStudioHomeData } from '../../studio-home/data/selectors'; +import { getWaffleFlags } from '../../data/selectors'; import { otherLinkURLParams } from './constants'; import messages from './messages'; import HelpSidebarLink from './HelpSidebarLink'; @@ -26,7 +26,7 @@ const HelpSidebar = ({ scheduleAndDetails, groupConfigurations, } = otherLinkURLParams; - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const showOtherLink = (params) => !pathname.includes(params); const generateLegacyURL = (urlParameter) => { diff --git a/src/header/Header.tsx b/src/header/Header.tsx index a1fe9b3df7..a5070fcf06 100644 --- a/src/header/Header.tsx +++ b/src/header/Header.tsx @@ -5,7 +5,7 @@ import { StudioHeader } from '@edx/frontend-component-header'; import { type Container, useToggle } from '@openedx/paragon'; import { generatePath, useHref, useNavigate } from 'react-router-dom'; -import { getStudioHomeData } from '../studio-home/data/selectors'; +import { getWaffleFlags } from '../data/selectors'; import { SearchModal } from '../search-modal'; import { useContentMenuItems, useSettingMenuItems, useToolsMenuItems } from './hooks'; import messages from './messages'; @@ -34,7 +34,7 @@ const Header = ({ const intl = useIntl(); const libraryHref = useHref('/library/:libraryId'); const navigate = useNavigate(); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const [isShowSearchModalOpen, openSearchModal, closeSearchModal] = useToggle(false); diff --git a/src/header/hooks.js b/src/header/hooks.js index d18a15e121..afd4a3c4fc 100644 --- a/src/header/hooks.js +++ b/src/header/hooks.js @@ -1,14 +1,16 @@ import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useSelector } from 'react-redux'; + import { getPagePath } from '../utils'; +import { getWaffleFlags } from '../data/selectors'; import { getStudioHomeData } from '../studio-home/data/selectors'; import messages from './messages'; export const useContentMenuItems = courseId => { const intl = useIntl(); const studioBaseUrl = getConfig().STUDIO_BASE_URL; - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const items = [ { @@ -42,7 +44,7 @@ export const useSettingMenuItems = courseId => { const intl = useIntl(); const studioBaseUrl = getConfig().STUDIO_BASE_URL; const { canAccessAdvancedSettings } = useSelector(getStudioHomeData); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const items = [ { @@ -80,7 +82,7 @@ export const useSettingMenuItems = courseId => { export const useToolsMenuItems = courseId => { const intl = useIntl(); const studioBaseUrl = getConfig().STUDIO_BASE_URL; - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const items = [ { diff --git a/src/pages-and-resources/pages/PageSettingButton.jsx b/src/pages-and-resources/pages/PageSettingButton.jsx index 0f20fc0c2d..952f85afc0 100644 --- a/src/pages-and-resources/pages/PageSettingButton.jsx +++ b/src/pages-and-resources/pages/PageSettingButton.jsx @@ -7,6 +7,7 @@ import { Icon, IconButton } from '@openedx/paragon'; import { ArrowForward, Settings } from '@openedx/paragon/icons'; import { useNavigate, Link } from 'react-router-dom'; +import { getWaffleFlags } from '../../data/selectors'; import messages from '../messages'; import { PagesAndResourcesContext } from '../PagesAndResourcesProvider'; @@ -19,7 +20,7 @@ const PageSettingButton = ({ const { formatMessage } = useIntl(); const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext); const navigate = useNavigate(); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const determineLinkDestination = useMemo(() => { if (!legacyLink) { return null; } diff --git a/src/pages-and-resources/pages/PageSettingButton.test.jsx b/src/pages-and-resources/pages/PageSettingButton.test.jsx index d9a22db14d..6def7abcb7 100644 --- a/src/pages-and-resources/pages/PageSettingButton.test.jsx +++ b/src/pages-and-resources/pages/PageSettingButton.test.jsx @@ -1,80 +1,86 @@ import { render, screen } from '@testing-library/react'; +import { useSelector } from 'react-redux'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { Provider } from 'react-redux'; -import { MemoryRouter } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; import PageSettingButton from './PageSettingButton'; -import messages from '../messages'; - -const mockStore = configureStore([]); - -const renderComponent = (props, store) => { - render( - - - - - - - , - ); + +jest.mock('react-redux', () => ({ + useSelector: jest.fn(), +})); + +jest.mock('react-router-dom', () => { + // eslint-disable-next-line global-require + const PropTypes = require('prop-types'); + + const Link = ({ children, to }) => {children}; + + Link.propTypes = { + children: PropTypes.node.isRequired, + to: PropTypes.string.isRequired, + }; + + return { + useNavigate: jest.fn(), + Link, + }; +}); + +const mockWaffleFlags = { + useNewTextbooksPage: true, + useNewCustomPages: true, }; -describe('PageSettingButton', () => { - let store; +const defaultProps = { + id: 'page_id', + courseId: 'course-v1:edX+DemoX+Demo_Course', + legacyLink: 'http://legacylink.com/tabs', + allowedOperations: { configure: true, enable: true }, +}; + +const renderComponent = (props = {}) => render( + + + , +); +describe('PageSettingButton', () => { beforeEach(() => { - store = mockStore({ - studioHomeData: { - waffleFlags: { - ENABLE_NEW_TEXTBOOKS_PAGE: true, - ENABLE_NEW_CUSTOM_PAGES: true, - }, - }, - }); + useSelector.mockClear(); }); - it('renders a link when legacyLink is provided and matches textbooks condition', () => { - renderComponent({ - id: 'page_1', - courseId: 'course_1', - legacyLink: 'textbooks', - allowedOperations: {}, - }, store); + it('renders the settings button with the new textbooks page link when useNewTextbooksPage is true', () => { + useSelector.mockReturnValue(mockWaffleFlags); + + renderComponent({ legacyLink: 'http://legacylink.com/textbooks' }); - expect(screen.getByRole('link')).toHaveAttribute('href', '/textbooks'); + const linkElement = screen.getByRole('link'); + expect(linkElement).toHaveAttribute('href', `/course/${defaultProps.courseId}/page-id`); }); - it('renders a link when legacyLink is provided and matches tabs condition', () => { - renderComponent({ - id: 'page_2', - courseId: 'course_2', - legacyLink: 'tabs', - allowedOperations: {}, - }, store); + it('renders the settings button with the legacy link when useNewTextbooksPage is false', () => { + useSelector.mockReturnValue({ ...mockWaffleFlags, useNewTextbooksPage: false }); - expect(screen.getByRole('link')).toHaveAttribute('href', '/tabs'); + renderComponent({ legacyLink: 'http://legacylink.com/textbooks' }); + + const linkElement = screen.getByRole('link'); + expect(linkElement).toHaveAttribute('href', 'http://legacylink.com/textbooks'); }); - it('renders an IconButton when allowedOperations allows configuration or enabling', () => { - renderComponent({ - id: 'page_3', - courseId: 'course_3', - allowedOperations: { configure: true }, - }, store); + it('renders the settings button with the new custom pages link when useNewCustomPages is true', () => { + useSelector.mockReturnValue(mockWaffleFlags); + + renderComponent(); - expect(screen.getByRole('button')).toBeInTheDocument(); + const linkElement = screen.getByRole('link'); + expect(linkElement).toHaveAttribute('href', `/course/${defaultProps.courseId}/page-id`); }); - it('does not render anything when neither legacyLink nor allowedOperations allows configuration or enabling', () => { - renderComponent({ - id: 'page_4', - courseId: 'course_4', - allowedOperations: {}, - }, store); + it('renders the settings button with the legacy link when useNewCustomPages is false', () => { + useSelector.mockReturnValue({ ...mockWaffleFlags, useNewCustomPages: false }); + + renderComponent(); - expect(screen.queryByRole('link')).not.toBeInTheDocument(); - expect(screen.queryByRole('button')).not.toBeInTheDocument(); + const linkElement = screen.getByRole('link'); + expect(linkElement).toHaveAttribute('href', defaultProps.legacyLink); }); }); diff --git a/src/studio-home/card-item/index.tsx b/src/studio-home/card-item/index.tsx index 4c26ebb8b8..514ffa686f 100644 --- a/src/studio-home/card-item/index.tsx +++ b/src/studio-home/card-item/index.tsx @@ -12,6 +12,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; import { Link } from 'react-router-dom'; +import { getWaffleFlags } from '../../data/selectors'; import { COURSE_CREATOR_STATES } from '../../constants'; import { getStudioHomeData } from '../data/selectors'; import messages from '../messages'; @@ -60,7 +61,7 @@ const CardItem: React.FC = ({ courseCreatorStatus, rerunCreatorStatus, } = useSelector(getStudioHomeData); - const waffleFlags = useSelector(state => state.courseDetail.waffleFlags); + const waffleFlags = useSelector(getWaffleFlags); const destinationUrl: string = waffleFlags?.useNewCourseOutlinePage ? path ?? url diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js index fcc6603979..b70af23ddb 100644 --- a/src/studio-home/data/api.js +++ b/src/studio-home/data/api.js @@ -13,7 +13,6 @@ export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).h */ export async function getStudioHomeData() { const { data } = await getAuthenticatedHttpClient().get(getStudioHomeApiUrl()); - return camelCaseObject(data); } diff --git a/src/studio-home/hooks.jsx b/src/studio-home/hooks.jsx index 584d69d842..4b50b02d22 100644 --- a/src/studio-home/hooks.jsx +++ b/src/studio-home/hooks.jsx @@ -6,6 +6,7 @@ import { RequestStatus } from '../data/constants'; import { COURSE_CREATOR_STATES } from '../constants'; import { getCourseData, getSavingStatus } from '../generic/data/selectors'; import { fetchStudioHomeData } from './data/thunks'; +import { fetchWaffleFlags } from '../data/thunks'; import { getLoadingStatuses, getSavingStatuses, @@ -36,6 +37,7 @@ const useStudioHome = (isPaginated = false) => { dispatch(fetchStudioHomeData(location.search ?? '')); setShowNewCourseContainer(false); } + dispatch(fetchWaffleFlags()); }, [location.search]); useEffect(() => { diff --git a/src/utils.js b/src/utils.js index 088f5bb56d..6cadfd9d9a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -108,7 +108,7 @@ export const createCorrectInternalRoute = (checkPath) => { } if (!checkPath.startsWith(basePath)) { - return `${checkPath}`; + return checkPath; } return checkPath;