refactor: Enable TypeScript support in this repo (#1459)
This commit is contained in:
1
Makefile
1
Makefile
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext({});
|
|
||||||
|
|
||||||
export default SidebarContext;
|
|
||||||
37
src/courseware/course/new-sidebar/SidebarContext.ts
Normal file
37
src/courseware/course/new-sidebar/SidebarContext.ts
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
const RightSidebarFilled = (props) => (
|
const RightSidebarFilled = (props) => (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={24}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
const RightSidebarOutlined = (props) => (
|
const RightSidebarOutlined = (props) => (
|
||||||
<svg
|
<svg
|
||||||
width={24}
|
width={24}
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
41
src/frontend-platform.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
17
src/utils.js
17
src/utils.js
@@ -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
23
src/utils.ts
Normal 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
13
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user