refactor: Enable TypeScript support in this repo (#1459)

This commit is contained in:
Braden MacDonald
2024-10-07 10:23:24 -07:00
committed by GitHub
parent 356b183c5c
commit 9a83d67d78
84 changed files with 213 additions and 134 deletions

View File

@@ -55,6 +55,7 @@ validate:
make validate-no-uncommitted-package-lock-changes make validate-no-uncommitted-package-lock-changes
npm run i18n_extract npm run i18n_extract
npm run lint -- --max-warnings 0 npm run lint -- --max-warnings 0
npm run types
npm run test npm run test
npm run build npm run build
npm run bundlewatch npm run bundlewatch

View File

@@ -13,14 +13,15 @@
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"bundlewatch": "bundlewatch", "bundlewatch": "bundlewatch",
"i18n_extract": "fedx-scripts formatjs extract", "i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .", "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "patch-package", "postinstall": "patch-package",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io", "dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests" "test": "fedx-scripts jest --coverage --passWithNoTests",
"types": "tsc --noEmit"
}, },
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",

View File

@@ -16,7 +16,7 @@ export const DECODE_ROUTES = {
], ],
REDIRECT_HOME: 'home/:courseId', REDIRECT_HOME: 'home/:courseId',
REDIRECT_SURVEY: 'survey/:courseId', REDIRECT_SURVEY: 'survey/:courseId',
}; } as const satisfies Readonly<{ [k: string]: string | readonly string[] }>;
export const ROUTES = { export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token', UNSUBSCRIBE: '/goal-unsubscribe/:token',
@@ -25,7 +25,7 @@ export const ROUTES = {
DASHBOARD: 'dashboard', DASHBOARD: 'dashboard',
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard', ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
CONSENT: 'consent', CONSENT: 'consent',
}; } as const satisfies Readonly<{ [k: string]: string }>;
export const REDIRECT_MODES = { export const REDIRECT_MODES = {
DASHBOARD_REDIRECT: 'dashboard-redirect', DASHBOARD_REDIRECT: 'dashboard-redirect',
@@ -33,7 +33,7 @@ export const REDIRECT_MODES = {
CONSENT_REDIRECT: 'consent-redirect', CONSENT_REDIRECT: 'consent-redirect',
HOME_REDIRECT: 'home-redirect', HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect', SURVEY_REDIRECT: 'survey-redirect',
}; } as const satisfies Readonly<{ [k: string]: string }>;
export const VERIFIED_MODES = [ export const VERIFIED_MODES = [
'professional', 'professional',
@@ -44,14 +44,15 @@ export const VERIFIED_MODES = [
'executive-education', 'executive-education',
'paid-executive-education', 'paid-executive-education',
'paid-bootcamp', 'paid-bootcamp',
]; ] as const satisfies readonly string[];
export const WIDGETS = { export const WIDGETS = {
DISCUSSIONS: 'DISCUSSIONS', DISCUSSIONS: 'DISCUSSIONS',
NOTIFICATIONS: 'NOTIFICATIONS', NOTIFICATIONS: 'NOTIFICATIONS',
}; } as const satisfies Readonly<{ [k: string]: string }>;
export const LOADING = 'loading'; export const LOADING = 'loading';
export const LOADED = 'loaded'; export const LOADED = 'loaded';
export const FAILED = 'failed'; export const FAILED = 'failed';
export const DENIED = 'denied'; export const DENIED = 'denied';
export type StatusValue = typeof LOADING | typeof LOADED | typeof FAILED | typeof DENIED;

View File

@@ -1,10 +1,12 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
export const LOADING = 'loading'; import {
export const LOADED = 'loaded'; LOADING,
export const FAILED = 'failed'; LOADED,
export const DENIED = 'denied'; FAILED,
DENIED,
} from '@src/constants';
const slice = createSlice({ const slice = createSlice({
name: 'course-home', name: 'course-home',

View File

@@ -12,9 +12,9 @@ import {
} from '@openedx/paragon'; } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import truncate from 'truncate-html'; import truncate from 'truncate-html';
import { FAILED, LOADED, LOADING } from '@src/constants';
import { useModel } from '../../../generic/model-store'; import { useModel } from '../../../generic/model-store';
import fetchCourseRecommendations from './data/thunks'; import fetchCourseRecommendations from './data/thunks';
import { FAILED, LOADED, LOADING } from './data/slice';
import CatalogSuggestion from './CatalogSuggestion'; import CatalogSuggestion from './CatalogSuggestion';
import PageLoading from '../../../generic/PageLoading'; import PageLoading from '../../../generic/PageLoading';
import { logClick } from './utils'; import { logClick } from './utils';

View File

@@ -1,9 +1,10 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import {
export const LOADING = 'loading'; LOADING,
export const LOADED = 'loaded'; LOADED,
export const FAILED = 'failed'; FAILED,
} from '@src/constants';
const slice = createSlice({ const slice = createSlice({
courseId: null, courseId: null,

View File

@@ -1,5 +0,0 @@
import React from 'react';
const SidebarContext = React.createContext({});
export default SidebarContext;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import type { WIDGETS } from '@src/constants';
import type { SIDEBARS } from './sidebars';
export type SidebarId = keyof typeof SIDEBARS;
export type WidgetId = keyof typeof WIDGETS;
export type UpgradeNotificationState = (
| 'accessLastHour'
| 'accessHoursLeft'
| 'accessDaysLeft'
| 'FPDdaysLeft'
| 'FPDLastHour'
| 'accessDateView'
| 'PastExpirationDate'
);
export interface SidebarContextData {
toggleSidebar: (sidebarId?: SidebarId | null, widgetId?: WidgetId | null) => void;
onNotificationSeen: () => void;
setNotificationStatus: React.Dispatch<'active' | 'inactive'>;
currentSidebar: SidebarId | null;
notificationStatus: 'active' | 'inactive';
upgradeNotificationCurrentState: UpgradeNotificationState;
setUpgradeNotificationCurrentState: React.Dispatch<UpgradeNotificationState>;
shouldDisplaySidebarOpen: boolean;
shouldDisplayFullScreen: boolean;
courseId: string;
unitId: string;
hideDiscussionbar: boolean;
hideNotificationbar: boolean;
isNotificationbarAvailable: boolean;
isDiscussionbarAvailable: boolean;
}
const SidebarContext = React.createContext<SidebarContextData>({} as SidebarContextData);
export default SidebarContext;

View File

@@ -1,7 +1,6 @@
import React, { import React, {
useCallback, useEffect, useMemo, useState, useCallback, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import PropTypes from 'prop-types';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
@@ -13,7 +12,13 @@ import { WIDGETS } from '../../../constants';
import SidebarContext from './SidebarContext'; import SidebarContext from './SidebarContext';
import { SIDEBARS } from './sidebars'; import { SIDEBARS } from './sidebars';
const SidebarProvider = ({ interface Props {
courseId: string;
unitId: string;
children?: React.ReactNode;
}
const SidebarProvider: React.FC<Props> = ({
courseId, courseId,
unitId, unitId,
children, children,
@@ -122,14 +127,4 @@ const SidebarProvider = ({
); );
}; };
SidebarProvider.propTypes = {
courseId: PropTypes.string.isRequired,
unitId: PropTypes.string.isRequired,
children: PropTypes.node,
};
SidebarProvider.defaultProps = {
children: null,
};
export default SidebarProvider; export default SidebarProvider;

View File

@@ -1,5 +1,4 @@
import React, { useCallback, useContext } from 'react'; import React, { useCallback, useContext } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -10,18 +9,30 @@ import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import { useEventListener } from '../../../../generic/hooks'; import { useEventListener } from '../../../../generic/hooks';
import { WIDGETS } from '../../../../constants'; import { WIDGETS } from '../../../../constants';
import messages from '../messages'; import messages from '../messages';
import SidebarContext from '../SidebarContext'; import SidebarContext, { type SidebarId } from '../SidebarContext';
const SidebarBase = ({ interface Props {
title, title?: string;
ariaLabel: string;
sidebarId: SidebarId;
className?: string;
children: React.ReactNode;
showTitleBar?: boolean;
width?: string;
allowFullHeight?: boolean;
showBorder?: boolean;
}
const SidebarBase: React.FC<Props> = ({
title = '',
ariaLabel, ariaLabel,
sidebarId, sidebarId,
className, className,
children, children,
showTitleBar, showTitleBar = true,
width, width = '45rem',
allowFullHeight, allowFullHeight = false,
showBorder, showBorder = true,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { const {
@@ -58,8 +69,7 @@ const SidebarBase = ({
onClick={() => toggleSidebar(null)} onClick={() => toggleSidebar(null)}
onKeyDown={() => toggleSidebar(null)} onKeyDown={() => toggleSidebar(null)}
role="button" role="button"
tabIndex="0" tabIndex={0}
alt={intl.formatMessage(messages.responsiveCloseSidebarTray)}
> >
<Icon src={ArrowBackIos} /> <Icon src={ArrowBackIos} />
<span className="font-weight-bold m-2 d-inline-block"> <span className="font-weight-bold m-2 d-inline-block">
@@ -90,25 +100,4 @@ const SidebarBase = ({
); );
}; };
SidebarBase.propTypes = {
title: PropTypes.string,
ariaLabel: PropTypes.string.isRequired,
sidebarId: PropTypes.string.isRequired,
className: PropTypes.string,
children: PropTypes.element.isRequired,
showTitleBar: PropTypes.bool,
width: PropTypes.string,
allowFullHeight: PropTypes.bool,
showBorder: PropTypes.bool,
};
SidebarBase.defaultProps = {
title: '',
width: '45rem',
allowFullHeight: false,
showTitleBar: true,
className: '',
showBorder: true,
};
export default SidebarBase; export default SidebarBase;

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
const RightSidebarFilled = (props) => ( const RightSidebarFilled = (props) => (
<svg <svg
width={24} width={24}

View File

@@ -1,5 +1,3 @@
import * as React from 'react';
const RightSidebarOutlined = (props) => ( const RightSidebarOutlined = (props) => (
<svg <svg
width={24} width={24}

View File

@@ -31,7 +31,7 @@ describe('DiscussionsWidget', () => {
excludeFetchSequence: false, excludeFetchSequence: false,
}); });
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const state = store.getState(); const state = store.getState() as any; // TODO: remove 'any' once redux state gets types
courseId = state.courseware.courseId; courseId = state.courseware.courseId;
[unitId] = Object.keys(state.models.units); [unitId] = Object.keys(state.models.units);

View File

@@ -14,7 +14,7 @@ import {
import initializeStore from '../../../../../../store'; import initializeStore from '../../../../../../store';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils'; import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
import { fetchCourse } from '../../../../../data'; import { fetchCourse } from '../../../../../data';
import SidebarContext from '../../../SidebarContext'; import SidebarContext, { SidebarContextData } from '../../../SidebarContext';
import NotificationsWidget from './NotificationsWidget'; import NotificationsWidget from './NotificationsWidget';
import setupDiscussionSidebar from '../../../../test-utils'; import setupDiscussionSidebar from '../../../../test-utils';
@@ -24,7 +24,7 @@ jest.mock('@edx/frontend-platform/analytics');
describe('NotificationsWidget', () => { describe('NotificationsWidget', () => {
let axiosMock; let axiosMock;
let store; let store;
const ID = 'NEWSIDEBAR'; const ID = 'DISCUSSIONS_NOTIFICATIONS';
const defaultMetadata = Factory.build('courseMetadata'); const defaultMetadata = Factory.build('courseMetadata');
const courseId = defaultMetadata.id; const courseId = defaultMetadata.id;
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`; let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
@@ -33,7 +33,7 @@ describe('NotificationsWidget', () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata'); const courseHomeMetadata = Factory.build('courseHomeMetadata');
const courseHomeMetadataUrl = appendBrowserTimezoneToUrl(`${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`); const courseHomeMetadataUrl = appendBrowserTimezoneToUrl(`${getConfig().LMS_BASE_URL}/api/course_home/course_metadata/${courseId}`);
function setMetadata(attributes, options) { function setMetadata(attributes, options = undefined) {
const updatedCourseHomeMetadata = Factory.build('courseHomeMetadata', attributes, options); const updatedCourseHomeMetadata = Factory.build('courseHomeMetadata', attributes, options);
axiosMock.onGet(courseHomeMetadataUrl).reply(200, updatedCourseHomeMetadata); axiosMock.onGet(courseHomeMetadataUrl).reply(200, updatedCourseHomeMetadata);
} }
@@ -85,7 +85,7 @@ describe('NotificationsWidget', () => {
courseId, courseId,
hideNotificationbar: false, hideNotificationbar: false,
isNotificationbarAvailable: true, isNotificationbarAvailable: true,
}} } as SidebarContextData}
> >
<NotificationsWidget /> <NotificationsWidget />
</SidebarContext.Provider>, </SidebarContext.Provider>,
@@ -94,14 +94,14 @@ describe('NotificationsWidget', () => {
}); });
it('renders upgrade card', async () => { it('renders upgrade card', async () => {
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
};
await fetchAndRender( await fetchAndRender(
<SidebarContext.Provider value={{ <SidebarContext.Provider value={contextData as SidebarContextData}>
currentSidebar: ID,
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
}}
>
<NotificationsWidget /> <NotificationsWidget />
</SidebarContext.Provider>, </SidebarContext.Provider>,
); );
@@ -116,14 +116,14 @@ describe('NotificationsWidget', () => {
it('renders no notifications bar if no verified mode', async () => { it('renders no notifications bar if no verified mode', async () => {
setMetadata({ verified_mode: null }); setMetadata({ verified_mode: null });
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
hideNotificationbar: true,
isNotificationbarAvailable: false,
};
await fetchAndRender( await fetchAndRender(
<SidebarContext.Provider value={{ <SidebarContext.Provider value={contextData as SidebarContextData}>
currentSidebar: ID,
courseId,
hideNotificationbar: true,
isNotificationbarAvailable: false,
}}
>
<NotificationsWidget /> <NotificationsWidget />
</SidebarContext.Provider>, </SidebarContext.Provider>,
); );
@@ -170,15 +170,15 @@ describe('NotificationsWidget', () => {
it('marks notification as seen 3 seconds later', async () => { it('marks notification as seen 3 seconds later', async () => {
const onNotificationSeen = jest.fn(); const onNotificationSeen = jest.fn();
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
onNotificationSeen,
hideNotificationbar: false,
isNotificationbarAvailable: true,
};
await fetchAndRender( await fetchAndRender(
<SidebarContext.Provider value={{ <SidebarContext.Provider value={contextData as SidebarContextData}>
currentSidebar: ID,
courseId,
onNotificationSeen,
hideNotificationbar: false,
isNotificationbarAvailable: true,
}}
>
<NotificationsWidget /> <NotificationsWidget />
</SidebarContext.Provider>, </SidebarContext.Provider>,
); );

View File

@@ -65,6 +65,7 @@ const NotificationsWidget = () => {
// After three seconds, update notificationSeen (to hide red dot) // After three seconds, update notificationSeen (to hide red dot)
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
setTimeout(onNotificationSeen, 3000); setTimeout(onNotificationSeen, 3000);
sendTrackEvent('edx.ui.course.upgrade.new_sidebar.notifications', notificationTrayEventProperties); sendTrackEvent('edx.ui.course.upgrade.new_sidebar.notifications', notificationTrayEventProperties);
}, []); }, []);
@@ -94,7 +95,6 @@ const NotificationsWidget = () => {
timeOffsetMillis={timeOffsetMillis} timeOffsetMillis={timeOffsetMillis}
courseId={courseId} courseId={courseId}
org={org} org={org}
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState} setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
toggleSidebar={onToggleSidebar} toggleSidebar={onToggleSidebar}
/> />

View File

@@ -6,8 +6,8 @@ export const SIDEBARS = {
Sidebar: discussionsNotifications.Sidebar, Sidebar: discussionsNotifications.Sidebar,
Trigger: discussionsNotifications.Trigger, Trigger: discussionsNotifications.Trigger,
}, },
}; } as const;
export const SIDEBAR_ORDER = [ export const SIDEBAR_ORDER = [
discussionsNotifications.ID, discussionsNotifications.ID,
]; ] as const;

View File

@@ -1,4 +1,4 @@
/* eslint-disable no-use-before-define */ /* eslint-disable @typescript-eslint/no-use-before-define */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';

View File

@@ -13,12 +13,12 @@ import {
import { PluginSlot } from '@openedx/frontend-plugin-framework'; import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { LOADED } from '@src/constants';
import { GetCourseExitNavigation } from '../../course-exit'; import { GetCourseExitNavigation } from '../../course-exit';
import UnitButton from './UnitButton'; import UnitButton from './UnitButton';
import SequenceNavigationTabs from './SequenceNavigationTabs'; import SequenceNavigationTabs from './SequenceNavigationTabs';
import { useSequenceNavigationMetadata } from './hooks'; import { useSequenceNavigationMetadata } from './hooks';
import { useModel } from '../../../../generic/model-store'; import { useModel } from '../../../../generic/model-store';
import { LOADED } from '../../../data/slice';
import messages from './messages'; import messages from './messages';

View File

@@ -9,7 +9,7 @@ import {
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import { useModel } from '@src/generic/model-store'; import { useModel } from '@src/generic/model-store';
import { LOADING, LOADED } from '@src/course-home/data/slice'; import { LOADING, LOADED } from '@src/constants';
import PageLoading from '@src/generic/PageLoading'; import PageLoading from '@src/generic/PageLoading';
import { import {
getSequenceId, getSequenceId,

View File

@@ -64,6 +64,7 @@ const NotificationTray = ({ intl }) => {
}; };
// After three seconds, update notificationSeen (to hide red dot) // After three seconds, update notificationSeen (to hide red dot)
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
setTimeout(onNotificationSeen, 3000); setTimeout(onNotificationSeen, 3000);
sendTrackEvent('edx.ui.course.upgrade.old_sidebar.notifications', notificationTrayEventProperties); sendTrackEvent('edx.ui.course.upgrade.old_sidebar.notifications', notificationTrayEventProperties);
}, []); }, []);

View File

@@ -4,8 +4,8 @@ import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { FAILED, LOADING } from '@src/constants';
import * as thunks from './thunks'; import * as thunks from './thunks';
import { FAILED, LOADING } from './slice';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils'; import { appendBrowserTimezoneToUrl, executeThunk } from '../../utils';

View File

@@ -1,4 +1,4 @@
import { LOADED } from './slice'; import { LOADED } from '@src/constants';
export function sequenceIdsSelector(state) { export function sequenceIdsSelector(state) {
if (state.courseware.courseStatus !== LOADED) { if (state.courseware.courseStatus !== LOADED) {

View File

@@ -1,10 +1,12 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
export const LOADING = 'loading'; import {
export const LOADED = 'loaded'; LOADING,
export const FAILED = 'failed'; LOADED,
export const DENIED = 'denied'; FAILED,
DENIED,
} from '@src/constants';
const slice = createSlice({ const slice = createSlice({
name: 'courseware', name: 'courseware',

41
src/frontend-platform.d.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
// frontend-platform currently doesn't provide types... do it ourselves for i18n module at least.
// We can remove this in the future when we migrate to frontend-shell, or when frontend-platform gets types
// (whichever comes first).
declare module '@edx/frontend-platform/i18n' {
// eslint-disable-next-line import/no-extraneous-dependencies
import { injectIntl as _injectIntl } from 'react-intl';
/** @deprecated Use useIntl() hook instead. */
export const injectIntl: typeof _injectIntl;
/** @deprecated Use useIntl() hook instead. */
export const intlShape: any;
// eslint-disable-next-line import/no-extraneous-dependencies
export {
createIntl,
FormattedDate,
FormattedTime,
FormattedRelativeTime,
FormattedNumber,
FormattedPlural,
FormattedMessage,
defineMessages,
IntlProvider,
useIntl,
} from 'react-intl';
// Other exports from the i18n module:
export const configure: any;
export const getPrimaryLanguageSubtag: (code: string) => string;
export const getLocale: (locale?: string) => string;
export const getMessages: any;
export const isRtl: (locale?: string) => boolean;
export const handleRtl: any;
export const mergeMessages: any;
export const LOCALE_CHANGED: any;
export const LOCALE_TOPIC: any;
export const getCountryList: any;
export const getCountryMessages: any;
export const getLanguageList: any;
export const getLanguageMessages: any;
}

View File

@@ -4,10 +4,10 @@ import { useParams, Navigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import FooterSlot from '@openedx/frontend-slot-footer'; import FooterSlot from '@openedx/frontend-slot-footer';
import { LOADED, LOADING } from '@src/constants';
import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert'; import useActiveEnterpriseAlert from '../alerts/active-enteprise-alert';
import { AlertList } from './user-messages'; import { AlertList } from './user-messages';
import { fetchDiscussionTab } from '../course-home/data/thunks'; import { fetchDiscussionTab } from '../course-home/data/thunks';
import { LOADED, LOADING } from '../course-home/data/slice';
import PageLoading from './PageLoading'; import PageLoading from './PageLoading';
import messages from '../tab-page/messages'; import messages from '../tab-page/messages';

View File

@@ -1,9 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { useIntl, FormattedDate, FormattedMessage } from '@edx/frontend-platform/i18n';
useIntl, FormattedDate, FormattedMessage, injectIntl,
} from '@edx/frontend-platform/i18n';
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { Button, Icon, IconButton } from '@openedx/paragon'; import { Button, Icon, IconButton } from '@openedx/paragon';
import { Close } from '@openedx/paragon/icons'; import { Close } from '@openedx/paragon/icons';
@@ -561,4 +559,4 @@ UpgradeNotification.defaultProps = {
toggleSidebar: null, toggleSidebar: null,
}; };
export default injectIntl(UpgradeNotification); export default UpgradeNotification;

View File

@@ -10,7 +10,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { logError } from '@edx/frontend-platform/logging'; import { logError } from '@edx/frontend-platform/logging';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { LOADED, LOADING, FAILED } from '../constants'; import { LOADED, LOADING, FAILED } from '@src/constants';
import PageLoading from '../generic/PageLoading'; import PageLoading from '../generic/PageLoading';
import { unsubscribeNotificationPreferences } from './data/api'; import { unsubscribeNotificationPreferences } from './data/api';
import messages from './messages'; import messages from './messages';

View File

@@ -193,10 +193,10 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
logUnhandledRequests(axiosMock); logUnhandledRequests(axiosMock);
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
!options.excludeFetchCourse && await executeThunk(fetchCourse(courseMetadata.id), store.dispatch); !options.excludeFetchCourse && await executeThunk(fetchCourse(courseMetadata.id), store.dispatch);
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
!options.excludeFetchOutlineSidebar && await executeThunk( !options.excludeFetchOutlineSidebar && await executeThunk(
getCourseOutlineStructure(courseMetadata.id), getCourseOutlineStructure(courseMetadata.id),
store.dispatch, store.dispatch,

View File

@@ -64,7 +64,7 @@ const StreakModal = ({
}) => { }) => {
const { org, celebrations, username } = useModel('courseHomeMeta', courseId); const { org, celebrations, username } = useModel('courseHomeMeta', courseId);
const factoid = getRandomFactoid(intl, streakLengthToCelebrate); const factoid = getRandomFactoid(intl, streakLengthToCelebrate);
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [randomFactoid, setRandomFactoid] = useState(factoid); // Don't change factoid on re-render const [randomFactoid, setRandomFactoid] = useState(factoid); // Don't change factoid on re-render
// Open edX Folks: if you create a voucher with this code, the MFE will notice and show the discount // Open edX Folks: if you create a voucher with this code, the MFE will notice and show the discount

View File

@@ -1,17 +0,0 @@
// Helper, that is used to forcibly finalize all promises
// in thunk before running matcher against state.
export const executeThunk = async (thunk, dispatch, getState) => {
await thunk(dispatch, getState);
await new Promise(setImmediate);
};
// Utility function for appending the browser timezone to the url
// Can be used on the backend when the user timezone is not set in the user account
export const appendBrowserTimezoneToUrl = (url) => {
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const urlObject = new URL(url);
if (browserTimezone) {
urlObject.searchParams.append('browser_timezone', browserTimezone);
}
return urlObject.href;
};

23
src/utils.ts Normal file
View File

@@ -0,0 +1,23 @@
/**
* Helper, that is used to forcibly finalize all promises
* in thunk before running matcher against state.
*
* TODO: move this to setupTest or testUtils - it's only used in tests.
*/
export const executeThunk = async (thunk, dispatch, getState = undefined) => {
await thunk(dispatch, getState);
await new Promise(setImmediate);
};
/**
* Utility function for appending the browser timezone to the url
* Can be used on the backend when the user timezone is not set in the user account
*/
export const appendBrowserTimezoneToUrl = (url: string) => {
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const urlObject = new URL(url);
if (browserTimezone) {
urlObject.searchParams.append('browser_timezone', browserTimezone);
}
return urlObject.href;
};

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "@edx/typescript-config",
"compilerOptions": {
"outDir": "dist",
"baseUrl": "./src",
"paths": {
"*": ["*"],
"@src/*": ["*"]
}
},
"include": ["*.js", ".eslintrc.js", "src/**/*", "plugins/**/*"],
"exclude": ["dist", "node_modules"]
}