feat(client): refactor Flash/redux into TS (#42725)
* feat(client): ts-migrate Flash/redux * add app types * convert Flash/redux/index to vanilla TS * update redux types.ts * use FlashState type over State type * update typing * fix: prettier errors I caused? * fix: re-add comment I removed * remove comment Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * fix type to not include {} * remove commented out code for future use * remove unused initialState object * rename Flash onClose prop to match action name * directly type reducer to return state Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -48,7 +48,7 @@ interface IShowCertificationProps {
|
|||||||
cert: CertType;
|
cert: CertType;
|
||||||
certDashedName: string;
|
certDashedName: string;
|
||||||
certSlug: string;
|
certSlug: string;
|
||||||
createFlashMessage: (payload: typeof standardErrorMessage) => void;
|
createFlashMessage: typeof createFlashMessage;
|
||||||
executeGA: (payload: Record<string, unknown>) => void;
|
executeGA: (payload: Record<string, unknown>) => void;
|
||||||
fetchProfileForUser: (username: string) => void;
|
fetchProfileForUser: (username: string) => void;
|
||||||
fetchState: {
|
fetchState: {
|
||||||
|
@ -29,7 +29,7 @@ const { apiLocation } = envData;
|
|||||||
|
|
||||||
// TODO: update types for actions
|
// TODO: update types for actions
|
||||||
interface IShowSettingsProps {
|
interface IShowSettingsProps {
|
||||||
createFlashMessage: (paylaod: string[]) => void;
|
createFlashMessage: typeof createFlashMessage;
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
navigate: (location: string) => void;
|
navigate: (location: string) => void;
|
||||||
showLoading: boolean;
|
showLoading: boolean;
|
||||||
|
@ -1,23 +1,19 @@
|
|||||||
import { Alert } from '@freecodecamp/react-bootstrap';
|
import { Alert } from '@freecodecamp/react-bootstrap';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||||
|
import { FlashState } from '../../redux/types';
|
||||||
|
import { removeFlashMessage } from './redux';
|
||||||
|
|
||||||
import './flash.css';
|
import './flash.css';
|
||||||
|
|
||||||
type FlashProps = {
|
type FlashProps = {
|
||||||
flashMessage: {
|
flashMessage: FlashState['message'];
|
||||||
type: string;
|
removeFlashMessage: typeof removeFlashMessage;
|
||||||
message: string;
|
|
||||||
id: string;
|
|
||||||
variables: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Flash({ flashMessage, onClose }: FlashProps): JSX.Element {
|
function Flash({ flashMessage, removeFlashMessage }: FlashProps): JSX.Element {
|
||||||
const { type, message, id, variables = {} } = flashMessage;
|
const { type, message, id, variables } = flashMessage;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [flashMessageHeight, setFlashMessageHeight] = useState(0);
|
const [flashMessageHeight, setFlashMessageHeight] = useState(0);
|
||||||
|
|
||||||
@ -33,7 +29,7 @@ function Flash({ flashMessage, onClose }: FlashProps): JSX.Element {
|
|||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
document.documentElement.style.setProperty('--flash-message-height', '0px');
|
document.documentElement.style.setProperty('--flash-message-height', '0px');
|
||||||
onClose();
|
removeFlashMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -62,14 +58,5 @@ function Flash({ flashMessage, onClose }: FlashProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Flash.displayName = 'FlashMessages';
|
Flash.displayName = 'FlashMessages';
|
||||||
Flash.propTypes = {
|
|
||||||
flashMessage: PropTypes.shape({
|
|
||||||
id: PropTypes.string,
|
|
||||||
type: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
variables: PropTypes.object
|
|
||||||
}),
|
|
||||||
onClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Flash;
|
export default Flash;
|
||||||
|
@ -1,41 +1,66 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
import { FlashState, State } from '../../../redux/types';
|
||||||
|
|
||||||
import { createTypes } from '../../../utils/create-types';
|
export const FlashApp = 'flash';
|
||||||
|
|
||||||
export const ns = 'flash';
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
message: {}
|
message: {
|
||||||
|
id: '',
|
||||||
|
type: '',
|
||||||
|
message: ''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const types = createTypes(
|
|
||||||
['createFlashMessage', 'removeFlashMessage'],
|
|
||||||
ns
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sagas = [];
|
export const sagas = [];
|
||||||
|
|
||||||
export const createFlashMessage = createAction(
|
export const flashMessageSelector = (state: State): FlashState['message'] =>
|
||||||
types.createFlashMessage,
|
state[FlashApp].message;
|
||||||
(msg: string[]) => ({ id: nanoid(), ...msg })
|
|
||||||
);
|
|
||||||
export const removeFlashMessage = createAction(types.removeFlashMessage);
|
|
||||||
|
|
||||||
// TODO: Once state is typed, add here, remove disable.
|
// ACTION DEFINITIONS
|
||||||
// eslint-disable-next-line
|
|
||||||
export const flashMessageSelector = (state: any): string => state[ns].message;
|
|
||||||
|
|
||||||
export const reducer = handleActions(
|
enum FlashActionTypes {
|
||||||
{
|
createFlashMessage = 'createFlashMessage',
|
||||||
[types.createFlashMessage]: (state, { payload }) => ({
|
removeFlashMessage = 'removeFlashMessage'
|
||||||
...state,
|
}
|
||||||
message: payload
|
|
||||||
}),
|
export type FlashMessageArg = {
|
||||||
[types.removeFlashMessage]: state => ({
|
type: string;
|
||||||
...state,
|
message: string;
|
||||||
message: {}
|
variables?: Record<string, unknown>;
|
||||||
})
|
};
|
||||||
},
|
|
||||||
initialState
|
export const createFlashMessage = (
|
||||||
);
|
flash: FlashMessageArg
|
||||||
|
): ReducerPayload<FlashActionTypes.createFlashMessage> => ({
|
||||||
|
type: FlashActionTypes.createFlashMessage,
|
||||||
|
payload: { ...flash, id: nanoid() }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const removeFlashMessage =
|
||||||
|
(): ReducerPayload<FlashActionTypes.removeFlashMessage> => ({
|
||||||
|
type: FlashActionTypes.removeFlashMessage
|
||||||
|
});
|
||||||
|
|
||||||
|
// REDUCER
|
||||||
|
type ReducerBase<T> = { type: T };
|
||||||
|
type ReducerPayload<T extends FlashActionTypes> =
|
||||||
|
T extends FlashActionTypes.createFlashMessage
|
||||||
|
? ReducerBase<T> & {
|
||||||
|
payload: FlashState['message'];
|
||||||
|
}
|
||||||
|
: ReducerBase<T>;
|
||||||
|
|
||||||
|
// Does reducer return FlashState or AppState (whole app)?
|
||||||
|
export const reducer = (
|
||||||
|
state: FlashState = initialState,
|
||||||
|
action: ReducerPayload<FlashActionTypes>
|
||||||
|
): FlashState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case FlashActionTypes.createFlashMessage:
|
||||||
|
return { ...state, message: action.payload };
|
||||||
|
case FlashActionTypes.removeFlashMessage:
|
||||||
|
return { ...state, message: initialState.message };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -217,7 +217,10 @@ class DefaultLayout extends Component {
|
|||||||
isSignedIn={isSignedIn}
|
isSignedIn={isSignedIn}
|
||||||
/>
|
/>
|
||||||
{hasMessage && flashMessage ? (
|
{hasMessage && flashMessage ? (
|
||||||
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
|
<Flash
|
||||||
|
flashMessage={flashMessage}
|
||||||
|
removeFlashMessage={removeFlashMessage}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@ import store from 'store';
|
|||||||
import { actionTypes as challengeTypes } from '../templates/Challenges/redux/action-types';
|
import { actionTypes as challengeTypes } from '../templates/Challenges/redux/action-types';
|
||||||
import { CURRENT_CHALLENGE_KEY } from '../templates/Challenges/redux/current-challenge-saga';
|
import { CURRENT_CHALLENGE_KEY } from '../templates/Challenges/redux/current-challenge-saga';
|
||||||
import { createAcceptTermsSaga } from './accept-terms-saga';
|
import { createAcceptTermsSaga } from './accept-terms-saga';
|
||||||
import { actionTypes, ns } from './action-types';
|
import { actionTypes } from './action-types';
|
||||||
import { createAppMountSaga } from './app-mount-saga';
|
import { createAppMountSaga } from './app-mount-saga';
|
||||||
import { createDonationSaga } from './donation-saga';
|
import { createDonationSaga } from './donation-saga';
|
||||||
import failedUpdatesEpic from './failed-updates-epic';
|
import failedUpdatesEpic from './failed-updates-epic';
|
||||||
@ -19,7 +19,7 @@ import { actionTypes as settingsTypes } from './settings/action-types';
|
|||||||
import { createShowCertSaga } from './show-cert-saga';
|
import { createShowCertSaga } from './show-cert-saga';
|
||||||
import updateCompleteEpic from './update-complete-epic';
|
import updateCompleteEpic from './update-complete-epic';
|
||||||
|
|
||||||
export { ns };
|
export const MainApp = 'app';
|
||||||
|
|
||||||
export const defaultFetchState = {
|
export const defaultFetchState = {
|
||||||
pending: true,
|
pending: true,
|
||||||
@ -172,8 +172,9 @@ export const updateCurrentChallengeId = createAction(
|
|||||||
|
|
||||||
export const completedChallengesSelector = state =>
|
export const completedChallengesSelector = state =>
|
||||||
userSelector(state).completedChallenges || [];
|
userSelector(state).completedChallenges || [];
|
||||||
export const completionCountSelector = state => state[ns].completionCount;
|
export const completionCountSelector = state => state[MainApp].completionCount;
|
||||||
export const currentChallengeIdSelector = state => state[ns].currentChallengeId;
|
export const currentChallengeIdSelector = state =>
|
||||||
|
state[MainApp].currentChallengeId;
|
||||||
export const stepsToClaimSelector = state => {
|
export const stepsToClaimSelector = state => {
|
||||||
const user = userSelector(state);
|
const user = userSelector(state);
|
||||||
const currentCerts = certificatesByNameSelector(user.username)(
|
const currentCerts = certificatesByNameSelector(user.username)(
|
||||||
@ -188,21 +189,24 @@ export const stepsToClaimSelector = state => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
export const isDonatingSelector = state => userSelector(state).isDonating;
|
||||||
export const isOnlineSelector = state => state[ns].isOnline;
|
export const isOnlineSelector = state => state[MainApp].isOnline;
|
||||||
export const isServerOnlineSelector = state => state[ns].isServerOnline;
|
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
|
||||||
export const isSignedInSelector = state => !!state[ns].appUsername;
|
export const isSignedInSelector = state => !!state[MainApp].appUsername;
|
||||||
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
|
export const isDonationModalOpenSelector = state =>
|
||||||
|
state[MainApp].showDonationModal;
|
||||||
export const recentlyClaimedBlockSelector = state =>
|
export const recentlyClaimedBlockSelector = state =>
|
||||||
state[ns].recentlyClaimedBlock;
|
state[MainApp].recentlyClaimedBlock;
|
||||||
export const donationFormStateSelector = state => state[ns].donationFormState;
|
export const donationFormStateSelector = state =>
|
||||||
|
state[MainApp].donationFormState;
|
||||||
export const signInLoadingSelector = state =>
|
export const signInLoadingSelector = state =>
|
||||||
userFetchStateSelector(state).pending;
|
userFetchStateSelector(state).pending;
|
||||||
export const showCertSelector = state => state[ns].showCert;
|
export const showCertSelector = state => state[MainApp].showCert;
|
||||||
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
|
export const showCertFetchStateSelector = state =>
|
||||||
|
state[MainApp].showCertFetchState;
|
||||||
export const shouldRequestDonationSelector = state => {
|
export const shouldRequestDonationSelector = state => {
|
||||||
const completedChallenges = completedChallengesSelector(state);
|
const completedChallenges = completedChallengesSelector(state);
|
||||||
const completionCount = completionCountSelector(state);
|
const completionCount = completionCountSelector(state);
|
||||||
const canRequestProgressDonation = state[ns].canRequestProgressDonation;
|
const canRequestProgressDonation = state[MainApp].canRequestProgressDonation;
|
||||||
const isDonating = isDonatingSelector(state);
|
const isDonating = isDonatingSelector(state);
|
||||||
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
|
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
|
||||||
|
|
||||||
@ -226,7 +230,7 @@ export const shouldRequestDonationSelector = state => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const userByNameSelector = username => state => {
|
export const userByNameSelector = username => state => {
|
||||||
const { user } = state[ns];
|
const { user } = state[MainApp];
|
||||||
// return initial state empty user empty object instead of empty
|
// return initial state empty user empty object instead of empty
|
||||||
// object litteral to prevent components from re-rendering unnecessarily
|
// object litteral to prevent components from re-rendering unnecessarily
|
||||||
return user[username] ?? initialState.user;
|
return user[username] ?? initialState.user;
|
||||||
@ -357,17 +361,17 @@ export const certificatesByNameSelector = username => state => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userFetchStateSelector = state => state[ns].userFetchState;
|
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
||||||
export const userProfileFetchStateSelector = state =>
|
export const userProfileFetchStateSelector = state =>
|
||||||
state[ns].userProfileFetchState;
|
state[MainApp].userProfileFetchState;
|
||||||
export const usernameSelector = state => state[ns].appUsername;
|
export const usernameSelector = state => state[MainApp].appUsername;
|
||||||
export const userSelector = state => {
|
export const userSelector = state => {
|
||||||
const username = usernameSelector(state);
|
const username = usernameSelector(state);
|
||||||
|
|
||||||
return state[ns].user[username] || {};
|
return state[MainApp].user[username] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sessionMetaSelector = state => state[ns].sessionMeta;
|
export const sessionMetaSelector = state => state[MainApp].sessionMeta;
|
||||||
|
|
||||||
function spreadThePayloadOnUser(state, payload) {
|
function spreadThePayloadOnUser(state, payload) {
|
||||||
return {
|
return {
|
||||||
|
@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
reducer as flash,
|
reducer as flash,
|
||||||
ns as flashNameSpace
|
FlashApp as flashNameSpace
|
||||||
} from '../components/Flash/redux';
|
} from '../components/Flash/redux';
|
||||||
import {
|
import {
|
||||||
reducer as search,
|
reducer as search,
|
||||||
@ -17,7 +17,7 @@ import {
|
|||||||
ns as curriculumMapNameSpace
|
ns as curriculumMapNameSpace
|
||||||
} from '../templates/Introduction/redux';
|
} from '../templates/Introduction/redux';
|
||||||
import { reducer as settings, ns as settingsNameSpace } from './settings';
|
import { reducer as settings, ns as settingsNameSpace } from './settings';
|
||||||
import { reducer as app, ns as appNameSpace } from './';
|
import { reducer as app, MainApp as appNameSpace } from './';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
[appNameSpace]: app,
|
[appNameSpace]: app,
|
||||||
|
56
client/src/redux/types.ts
Normal file
56
client/src/redux/types.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { FlashApp, FlashMessageArg } from '../components/Flash/redux';
|
||||||
|
import { MainApp } from '.';
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
[FlashApp]: FlashState;
|
||||||
|
[MainApp]: {
|
||||||
|
appUsername: string;
|
||||||
|
recentlyClaimedBlock: null | string;
|
||||||
|
canRequestProgressDonation: boolean;
|
||||||
|
completionCount: number;
|
||||||
|
currentChallengId: string;
|
||||||
|
showCert: Record<string, unknown>;
|
||||||
|
showCertFetchState: DefaultFetchState;
|
||||||
|
user: Record<string, unknown>;
|
||||||
|
userFetchState: DefaultFetchState;
|
||||||
|
userProfileFetchState: DefaultFetchState;
|
||||||
|
sessionMeta: {
|
||||||
|
activeDonations: number;
|
||||||
|
};
|
||||||
|
showDonationModal: boolean;
|
||||||
|
isOnline: boolean;
|
||||||
|
donationFormState: DefaultDonationFormState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlashState {
|
||||||
|
message: { id: string } & FlashMessageArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultFetchState {
|
||||||
|
pending: boolean;
|
||||||
|
complete: boolean;
|
||||||
|
errored: boolean;
|
||||||
|
error: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultDonationFormState {
|
||||||
|
redirecting: boolean;
|
||||||
|
processing: boolean;
|
||||||
|
success: boolean;
|
||||||
|
error: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFetchState = {
|
||||||
|
pending: true,
|
||||||
|
complete: false,
|
||||||
|
errored: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultDonationFormState = {
|
||||||
|
redirecting: false,
|
||||||
|
processing: false,
|
||||||
|
success: false,
|
||||||
|
error: ''
|
||||||
|
};
|
Reference in New Issue
Block a user