Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable TypeScript support in this repo #1459

Merged
merged 5 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ validate:
make validate-no-uncommitted-package-lock-changes
npm run i18n_extract
npm run lint -- --max-warnings 0
npm run types
npm run test
npm run build

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"postinstall": "patch-package",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
"test": "fedx-scripts jest --coverage --passWithNoTests",
"types": "tsc --noEmit"
},
"author": "edX",
"license": "AGPL-3.0",
Expand Down
11 changes: 6 additions & 5 deletions src/constants.js → src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const DECODE_ROUTES = {
],
REDIRECT_HOME: 'home/:courseId',
REDIRECT_SURVEY: 'survey/:courseId',
};
} as const satisfies Readonly<{ [k: string]: string | readonly string[] }>;

export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
Expand All @@ -25,15 +25,15 @@ export const ROUTES = {
DASHBOARD: 'dashboard',
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
CONSENT: 'consent',
};
} as const satisfies Readonly<{ [k: string]: string }>;

export const REDIRECT_MODES = {
DASHBOARD_REDIRECT: 'dashboard-redirect',
ENTERPRISE_LEARNER_DASHBOARD_REDIRECT: 'enterprise-learner-dashboard-redirect',
CONSENT_REDIRECT: 'consent-redirect',
HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect',
};
} as const satisfies Readonly<{ [k: string]: string }>;

export const VERIFIED_MODES = [
'professional',
Expand All @@ -44,14 +44,15 @@ export const VERIFIED_MODES = [
'executive-education',
'paid-executive-education',
'paid-bootcamp',
];
] as const satisfies readonly string[];

export const WIDGETS = {
DISCUSSIONS: 'DISCUSSIONS',
NOTIFICATIONS: 'NOTIFICATIONS',
};
} as const satisfies Readonly<{ [k: string]: string }>;
Comment on lines 49 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export const WIDGETS: Readonly<{ [k: string]: string }> = {
  DISCUSSIONS: 'DISCUSSIONS',
  NOTIFICATIONS: 'NOTIFICATIONS',
};

@bradenmacdonald I just experimented with this and I found this to be more precise, what do you say?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems less precise to me? If you do that, the type actually just becomes this, which is basically "any object" - not very useful:

Screenshot

Whereas using the satisfies operator, we get the exactly correct type as a constant:

Screenshot

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradenmacdonald That makes more sense I was looking it from the point of view of just reading it, inferencing it will be more helpful for sure in the way you did it.


export const LOADING = 'loading';
export const LOADED = 'loaded';
export const FAILED = 'failed';
export const DENIED = 'denied';
export type StatusValue = typeof LOADING | typeof LOADED | typeof FAILED | typeof DENIED;
10 changes: 6 additions & 4 deletions src/course-home/data/slice.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';

export const LOADING = 'loading';
export const LOADED = 'loaded';
export const FAILED = 'failed';
export const DENIED = 'denied';
import {
LOADING,
LOADED,
FAILED,
DENIED,
} from '@src/constants';

const slice = createSlice({
name: 'course-home',
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
} from '@openedx/paragon';
import PropTypes from 'prop-types';
import truncate from 'truncate-html';
import { FAILED, LOADED, LOADING } from '@src/constants';
import { useModel } from '../../../generic/model-store';
import fetchCourseRecommendations from './data/thunks';
import { FAILED, LOADED, LOADING } from './data/slice';
import CatalogSuggestion from './CatalogSuggestion';
import PageLoading from '../../../generic/PageLoading';
import { logClick } from './utils';
Expand Down
9 changes: 5 additions & 4 deletions src/courseware/course/course-exit/data/slice.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';

export const LOADING = 'loading';
export const LOADED = 'loaded';
export const FAILED = 'failed';
import {
LOADING,
LOADED,
FAILED,
} from '@src/constants';

const slice = createSlice({
courseId: null,
Expand Down
File renamed without changes.
5 changes: 0 additions & 5 deletions src/courseware/course/new-sidebar/SidebarContext.js

This file was deleted.

37 changes: 37 additions & 0 deletions src/courseware/course/new-sidebar/SidebarContext.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {
useCallback, useEffect, useMemo, useState,
} from 'react';
import PropTypes from 'prop-types';

import isEmpty from 'lodash/isEmpty';

Expand All @@ -13,7 +12,13 @@ import { WIDGETS } from '../../../constants';
import SidebarContext from './SidebarContext';
import { SIDEBARS } from './sidebars';

const SidebarProvider = ({
interface Props {
courseId: string;
unitId: string;
children?: React.ReactNode;
}

const SidebarProvider: React.FC<Props> = ({
courseId,
unitId,
children,
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useCallback, useContext } from 'react';
import PropTypes from 'prop-types';

import classNames from 'classnames';

Expand All @@ -10,18 +9,30 @@ import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import { useEventListener } from '../../../../generic/hooks';
import { WIDGETS } from '../../../../constants';
import messages from '../messages';
import SidebarContext from '../SidebarContext';
import SidebarContext, { type SidebarId } from '../SidebarContext';

const SidebarBase = ({
title,
interface Props {
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,
sidebarId,
className,
children,
showTitleBar,
width,
allowFullHeight,
showBorder,
showTitleBar = true,
width = '45rem',
allowFullHeight = false,
showBorder = true,
}) => {
const intl = useIntl();
const {
Expand Down Expand Up @@ -58,8 +69,7 @@ const SidebarBase = ({
onClick={() => toggleSidebar(null)}
onKeyDown={() => toggleSidebar(null)}
role="button"
tabIndex="0"
alt={intl.formatMessage(messages.responsiveCloseSidebarTray)}
tabIndex={0}
>
<Icon src={ArrowBackIos} />
<span className="font-weight-bold m-2 d-inline-block">
Expand Down Expand Up @@ -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;
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as React from 'react';

const RightSidebarFilled = (props) => (
<svg
width={24}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as React from 'react';

const RightSidebarOutlined = (props) => (
<svg
width={24}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('DiscussionsWidget', () => {
excludeFetchSequence: false,
});
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;
[unitId] = Object.keys(state.models.units);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import initializeStore from '../../../../../../store';
import { appendBrowserTimezoneToUrl, executeThunk } from '../../../../../../utils';
import { fetchCourse } from '../../../../../data';
import SidebarContext from '../../../SidebarContext';
import SidebarContext, { SidebarContextData } from '../../../SidebarContext';
import NotificationsWidget from './NotificationsWidget';
import setupDiscussionSidebar from '../../../../test-utils';

Expand All @@ -24,7 +24,7 @@ jest.mock('@edx/frontend-platform/analytics');
describe('NotificationsWidget', () => {
let axiosMock;
let store;
const ID = 'NEWSIDEBAR';
const ID = 'DISCUSSIONS_NOTIFICATIONS';
const defaultMetadata = Factory.build('courseMetadata');
const courseId = defaultMetadata.id;
let courseMetadataUrl = `${getConfig().LMS_BASE_URL}/api/courseware/course/${defaultMetadata.id}`;
Expand All @@ -33,7 +33,7 @@ describe('NotificationsWidget', () => {
const courseHomeMetadata = Factory.build('courseHomeMetadata');
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);
axiosMock.onGet(courseHomeMetadataUrl).reply(200, updatedCourseHomeMetadata);
}
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('NotificationsWidget', () => {
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
}}
} as SidebarContextData}
>
<NotificationsWidget />
</SidebarContext.Provider>,
Expand All @@ -94,14 +94,14 @@ describe('NotificationsWidget', () => {
});

it('renders upgrade card', async () => {
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
};
await fetchAndRender(
<SidebarContext.Provider value={{
currentSidebar: ID,
courseId,
hideNotificationbar: false,
isNotificationbarAvailable: true,
}}
>
<SidebarContext.Provider value={contextData as SidebarContextData}>
<NotificationsWidget />
</SidebarContext.Provider>,
);
Expand All @@ -116,14 +116,14 @@ describe('NotificationsWidget', () => {

it('renders no notifications bar if no verified mode', async () => {
setMetadata({ verified_mode: null });
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
hideNotificationbar: true,
isNotificationbarAvailable: false,
};
await fetchAndRender(
<SidebarContext.Provider value={{
currentSidebar: ID,
courseId,
hideNotificationbar: true,
isNotificationbarAvailable: false,
}}
>
<SidebarContext.Provider value={contextData as SidebarContextData}>
<NotificationsWidget />
</SidebarContext.Provider>,
);
Expand Down Expand Up @@ -170,15 +170,15 @@ describe('NotificationsWidget', () => {

it('marks notification as seen 3 seconds later', async () => {
const onNotificationSeen = jest.fn();
const contextData: Partial<SidebarContextData> = {
currentSidebar: ID,
courseId,
onNotificationSeen,
hideNotificationbar: false,
isNotificationbarAvailable: true,
};
await fetchAndRender(
<SidebarContext.Provider value={{
currentSidebar: ID,
courseId,
onNotificationSeen,
hideNotificationbar: false,
isNotificationbarAvailable: true,
}}
>
<SidebarContext.Provider value={contextData as SidebarContextData}>
<NotificationsWidget />
</SidebarContext.Provider>,
);
Expand Down
Loading
Loading