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;
|
||||
certDashedName: string;
|
||||
certSlug: string;
|
||||
createFlashMessage: (payload: typeof standardErrorMessage) => void;
|
||||
createFlashMessage: typeof createFlashMessage;
|
||||
executeGA: (payload: Record<string, unknown>) => void;
|
||||
fetchProfileForUser: (username: string) => void;
|
||||
fetchState: {
|
||||
|
@ -29,7 +29,7 @@ const { apiLocation } = envData;
|
||||
|
||||
// TODO: update types for actions
|
||||
interface IShowSettingsProps {
|
||||
createFlashMessage: (paylaod: string[]) => void;
|
||||
createFlashMessage: typeof createFlashMessage;
|
||||
isSignedIn: boolean;
|
||||
navigate: (location: string) => void;
|
||||
showLoading: boolean;
|
||||
|
@ -1,23 +1,19 @@
|
||||
import { Alert } from '@freecodecamp/react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import { FlashState } from '../../redux/types';
|
||||
import { removeFlashMessage } from './redux';
|
||||
|
||||
import './flash.css';
|
||||
|
||||
type FlashProps = {
|
||||
flashMessage: {
|
||||
type: string;
|
||||
message: string;
|
||||
id: string;
|
||||
variables: Record<string, unknown>;
|
||||
};
|
||||
onClose: () => void;
|
||||
flashMessage: FlashState['message'];
|
||||
removeFlashMessage: typeof removeFlashMessage;
|
||||
};
|
||||
|
||||
function Flash({ flashMessage, onClose }: FlashProps): JSX.Element {
|
||||
const { type, message, id, variables = {} } = flashMessage;
|
||||
function Flash({ flashMessage, removeFlashMessage }: FlashProps): JSX.Element {
|
||||
const { type, message, id, variables } = flashMessage;
|
||||
const { t } = useTranslation();
|
||||
const [flashMessageHeight, setFlashMessageHeight] = useState(0);
|
||||
|
||||
@ -33,7 +29,7 @@ function Flash({ flashMessage, onClose }: FlashProps): JSX.Element {
|
||||
|
||||
function handleClose() {
|
||||
document.documentElement.style.setProperty('--flash-message-height', '0px');
|
||||
onClose();
|
||||
removeFlashMessage();
|
||||
}
|
||||
|
||||
return (
|
||||
@ -62,14 +58,5 @@ function Flash({ flashMessage, onClose }: FlashProps): JSX.Element {
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -1,41 +1,66 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
import { FlashState, State } from '../../../redux/types';
|
||||
|
||||
import { createTypes } from '../../../utils/create-types';
|
||||
|
||||
export const ns = 'flash';
|
||||
export const FlashApp = 'flash';
|
||||
|
||||
const initialState = {
|
||||
message: {}
|
||||
message: {
|
||||
id: '',
|
||||
type: '',
|
||||
message: ''
|
||||
}
|
||||
};
|
||||
|
||||
export const types = createTypes(
|
||||
['createFlashMessage', 'removeFlashMessage'],
|
||||
ns
|
||||
);
|
||||
|
||||
export const sagas = [];
|
||||
|
||||
export const createFlashMessage = createAction(
|
||||
types.createFlashMessage,
|
||||
(msg: string[]) => ({ id: nanoid(), ...msg })
|
||||
);
|
||||
export const removeFlashMessage = createAction(types.removeFlashMessage);
|
||||
export const flashMessageSelector = (state: State): FlashState['message'] =>
|
||||
state[FlashApp].message;
|
||||
|
||||
// TODO: Once state is typed, add here, remove disable.
|
||||
// eslint-disable-next-line
|
||||
export const flashMessageSelector = (state: any): string => state[ns].message;
|
||||
// ACTION DEFINITIONS
|
||||
|
||||
export const reducer = handleActions(
|
||||
{
|
||||
[types.createFlashMessage]: (state, { payload }) => ({
|
||||
...state,
|
||||
message: payload
|
||||
}),
|
||||
[types.removeFlashMessage]: state => ({
|
||||
...state,
|
||||
message: {}
|
||||
})
|
||||
},
|
||||
initialState
|
||||
);
|
||||
enum FlashActionTypes {
|
||||
createFlashMessage = 'createFlashMessage',
|
||||
removeFlashMessage = 'removeFlashMessage'
|
||||
}
|
||||
|
||||
export type FlashMessageArg = {
|
||||
type: string;
|
||||
message: string;
|
||||
variables?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
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}
|
||||
/>
|
||||
{hasMessage && flashMessage ? (
|
||||
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
|
||||
<Flash
|
||||
flashMessage={flashMessage}
|
||||
removeFlashMessage={removeFlashMessage}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@ import store from 'store';
|
||||
import { actionTypes as challengeTypes } from '../templates/Challenges/redux/action-types';
|
||||
import { CURRENT_CHALLENGE_KEY } from '../templates/Challenges/redux/current-challenge-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 { createDonationSaga } from './donation-saga';
|
||||
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 updateCompleteEpic from './update-complete-epic';
|
||||
|
||||
export { ns };
|
||||
export const MainApp = 'app';
|
||||
|
||||
export const defaultFetchState = {
|
||||
pending: true,
|
||||
@ -172,8 +172,9 @@ export const updateCurrentChallengeId = createAction(
|
||||
|
||||
export const completedChallengesSelector = state =>
|
||||
userSelector(state).completedChallenges || [];
|
||||
export const completionCountSelector = state => state[ns].completionCount;
|
||||
export const currentChallengeIdSelector = state => state[ns].currentChallengeId;
|
||||
export const completionCountSelector = state => state[MainApp].completionCount;
|
||||
export const currentChallengeIdSelector = state =>
|
||||
state[MainApp].currentChallengeId;
|
||||
export const stepsToClaimSelector = state => {
|
||||
const user = userSelector(state);
|
||||
const currentCerts = certificatesByNameSelector(user.username)(
|
||||
@ -188,21 +189,24 @@ export const stepsToClaimSelector = state => {
|
||||
};
|
||||
};
|
||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
||||
export const isOnlineSelector = state => state[ns].isOnline;
|
||||
export const isServerOnlineSelector = state => state[ns].isServerOnline;
|
||||
export const isSignedInSelector = state => !!state[ns].appUsername;
|
||||
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
|
||||
export const isOnlineSelector = state => state[MainApp].isOnline;
|
||||
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
|
||||
export const isSignedInSelector = state => !!state[MainApp].appUsername;
|
||||
export const isDonationModalOpenSelector = state =>
|
||||
state[MainApp].showDonationModal;
|
||||
export const recentlyClaimedBlockSelector = state =>
|
||||
state[ns].recentlyClaimedBlock;
|
||||
export const donationFormStateSelector = state => state[ns].donationFormState;
|
||||
state[MainApp].recentlyClaimedBlock;
|
||||
export const donationFormStateSelector = state =>
|
||||
state[MainApp].donationFormState;
|
||||
export const signInLoadingSelector = state =>
|
||||
userFetchStateSelector(state).pending;
|
||||
export const showCertSelector = state => state[ns].showCert;
|
||||
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
|
||||
export const showCertSelector = state => state[MainApp].showCert;
|
||||
export const showCertFetchStateSelector = state =>
|
||||
state[MainApp].showCertFetchState;
|
||||
export const shouldRequestDonationSelector = state => {
|
||||
const completedChallenges = completedChallengesSelector(state);
|
||||
const completionCount = completionCountSelector(state);
|
||||
const canRequestProgressDonation = state[ns].canRequestProgressDonation;
|
||||
const canRequestProgressDonation = state[MainApp].canRequestProgressDonation;
|
||||
const isDonating = isDonatingSelector(state);
|
||||
const recentlyClaimedBlock = recentlyClaimedBlockSelector(state);
|
||||
|
||||
@ -226,7 +230,7 @@ export const shouldRequestDonationSelector = state => {
|
||||
};
|
||||
|
||||
export const userByNameSelector = username => state => {
|
||||
const { user } = state[ns];
|
||||
const { user } = state[MainApp];
|
||||
// return initial state empty user empty object instead of empty
|
||||
// object litteral to prevent components from re-rendering unnecessarily
|
||||
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 =>
|
||||
state[ns].userProfileFetchState;
|
||||
export const usernameSelector = state => state[ns].appUsername;
|
||||
state[MainApp].userProfileFetchState;
|
||||
export const usernameSelector = state => state[MainApp].appUsername;
|
||||
export const userSelector = 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) {
|
||||
return {
|
||||
|
@ -2,7 +2,7 @@ import { combineReducers } from 'redux';
|
||||
|
||||
import {
|
||||
reducer as flash,
|
||||
ns as flashNameSpace
|
||||
FlashApp as flashNameSpace
|
||||
} from '../components/Flash/redux';
|
||||
import {
|
||||
reducer as search,
|
||||
@ -17,7 +17,7 @@ import {
|
||||
ns as curriculumMapNameSpace
|
||||
} from '../templates/Introduction/redux';
|
||||
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({
|
||||
[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