Skip to content

Commit

Permalink
Merge pull request #331 from labzero/jeffrey/actions
Browse files Browse the repository at this point in the history
Silence Redux warnings by not storing functions in the state
  • Loading branch information
JeffreyATW authored Jun 20, 2023
2 parents 121f700 + b16aaac commit 37ab829
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 61 deletions.
22 changes: 22 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ThunkAction } from "@reduxjs/toolkit";
import { Action, State } from "../interfaces";
import { removeRestaurant } from "./restaurants";
import { removeTag } from "./tags";
import { changeUserRole, removeUser } from "./users";

const generateConfirmableActions = <
T extends {
[K in keyof T]: (
...args: Parameters<T[K]>
) => ThunkAction<Promise<Action>, State, unknown, Action>;
}
>(
actions: T
) => actions;

export const confirmableActions = generateConfirmableActions({
changeUserRole,
removeRestaurant,
removeTag,
removeUser,
});
6 changes: 5 additions & 1 deletion src/actions/modals.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { confirmableActions } from ".";
import { Action, ConfirmOpts, PastDecisionsOpts } from "../interfaces";

export function showModal(name: string): Action;
export function showModal(
name: "pastDecisions",
opts?: PastDecisionsOpts
): Action;
export function showModal(name: "confirm", opts?: ConfirmOpts): Action;
export function showModal(
name: "confirm",
opts?: ConfirmOpts<keyof typeof confirmableActions>
): Action;

export function showModal(name: unknown, opts?: unknown): unknown {
return {
Expand Down
2 changes: 1 addition & 1 deletion src/actions/restaurants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ export function addRestaurant(

export function removeRestaurant(
id: number
): ThunkAction<Promise<void>, State, unknown, Action> {
): ThunkAction<Promise<Action>, State, unknown, Action> {
return (dispatch) => {
dispatch(deleteRestaurant(id));
return fetch(`/api/restaurants/${id}`, {
Expand Down
4 changes: 2 additions & 2 deletions src/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function userDeleted(id: number, team: Team, isSelf: boolean): Action {
export function removeUser(
id: number,
team: Team
): ThunkAction<void, State, unknown, Action> {
): ThunkAction<Promise<Action>, State, unknown, Action> {
return (dispatch, getState) => {
const state = getState();
let isSelf = false;
Expand Down Expand Up @@ -182,7 +182,7 @@ export function userPatched(
export function changeUserRole(
id: number,
type: RoleType
): ThunkAction<void, State, unknown, Action> {
): ThunkAction<Promise<Action>, State, unknown, Action> {
const payload = { id, type };
return (dispatch, getState) => {
const state = getState();
Expand Down
26 changes: 16 additions & 10 deletions src/components/ConfirmModal/ConfirmModalContainer.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
import { connect } from "react-redux";
import { confirmableActions } from "../../actions";
import { hideModal } from "../../actions/modals";
import ConfirmModal from "./ConfirmModal";
import { Dispatch, State } from "../../interfaces";
import {
ConfirmModal as ConfirmModalType,
Dispatch,
State,
} from "../../interfaces";

const modalName = "confirm";

const mapStateToProps = (state: State) => ({
actionLabel: state.modals[modalName].actionLabel!,
body: state.modals[modalName].body,
action: state.modals[modalName].action,
shown: !!state.modals[modalName].shown,
});
const mapStateToProps = <T extends keyof typeof confirmableActions>(
state: State
) => state.modals[modalName] as ConfirmModalType<T>;

const mapDispatchToProps = (dispatch: Dispatch) => ({
dispatch,
hideModal: () => dispatch(hideModal("confirm")),
});

const mergeProps = (
stateProps: ReturnType<typeof mapStateToProps>,
const mergeProps = <T extends keyof typeof confirmableActions>(
stateProps: ConfirmModalType<T>,
dispatchProps: ReturnType<typeof mapDispatchToProps>
) => ({
...stateProps,
...dispatchProps,
handleSubmit: () => {
dispatchProps.dispatch(stateProps.action!);
dispatchProps.dispatch(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
confirmableActions[stateProps.action](...stateProps.actionArgs)
);
dispatchProps.hideModal();
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { connect } from "react-redux";
import { decide } from "../../actions/decisions";
import { hideModal } from "../../actions/modals";
import { Dispatch, State } from "../../interfaces";
import {
Dispatch,
State,
PastDecisionsModal as PastDecisionsModalType,
} from "../../interfaces";
import { getDecisionsByDay } from "../../selectors/decisions";
import { getRestaurantEntities } from "../../selectors/restaurants";
import PastDecisionsModal from "./PastDecisionsModal";
Expand All @@ -10,7 +14,8 @@ const modalName = "pastDecisions";

const mapStateToProps = (state: State) => ({
decisionsByDay: getDecisionsByDay(state),
restaurantId: state.modals[modalName].restaurantId,
restaurantId: (state.modals[modalName] as PastDecisionsModalType)
.restaurantId,
restaurantEntities: getRestaurantEntities(state),
shown: !!state.modals[modalName].shown,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ const mergeProps = (
showModal("confirm", {
actionLabel: "Delete",
body: `Are you sure you want to delete ${stateProps.restaurant.name}?`,
action: removeRestaurant(ownProps.id),
action: "removeRestaurant",
actionArgs: [ownProps.id],
})
),
showEditNameForm: () => {
Expand Down
8 changes: 4 additions & 4 deletions src/components/TagManagerItem/TagManagerItemContainer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { connect } from "react-redux";
import { getTagById } from "../../selectors/tags";
import { showModal } from "../../actions/modals";
import { removeTag } from "../../actions/tags";
import { Dispatch, State } from "../../interfaces";
import { ConfirmOpts, Dispatch, State } from "../../interfaces";
import TagManagerItem from "./TagManagerItem";

interface OwnProps {
Expand All @@ -28,11 +27,12 @@ const mergeProps = (
handleDeleteClicked() {
dispatchProps.dispatch(
showModal("confirm", {
action: "removeTag",
actionArgs: [ownProps.id],
actionLabel: "Delete",
body: `Are you sure you want to delete the “${stateProps.tag.name}” tag?
All restaurants will be untagged.`,
action: removeTag(ownProps.id),
})
} as ConfirmOpts<"removeTag">)
);
},
});
Expand Down
32 changes: 19 additions & 13 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Application, RequestHandler } from "express";
import { EnhancedStore, ThunkAction, ThunkDispatch } from "@reduxjs/toolkit";
import { EnhancedStore, ThunkDispatch } from "@reduxjs/toolkit";
import { BrowserHistory } from "history";
import { InsertCSS } from "isomorphic-style-loader/StyleContext";
import { ReactNode } from "react";
import { ResolveContext } from "universal-router";
import { WebSocket } from "ws";
import { confirmableActions } from "./actions";
import {
Decision as DecisionModel,
Restaurant as RestaurantModel,
Expand Down Expand Up @@ -387,7 +388,7 @@ export type Action =
| {
type: "SHOW_MODAL";
name: "confirm";
opts: ConfirmOpts;
opts: ConfirmOpts<keyof typeof confirmableActions>;
}
| {
type: "SHOW_MODAL";
Expand Down Expand Up @@ -451,12 +452,25 @@ export interface Notification {
);
}

export type ConfirmOpts = {
export type ConfirmOpts<T extends keyof typeof confirmableActions> = {
actionLabel: string;
body: string;
action: Action | ThunkAction<void, State, unknown, Action>;
action: T;
actionArgs: Parameters<(typeof confirmableActions)[T]>;
};

export type BaseModal = {
shown: boolean;
};

export type ConfirmModal<T extends keyof typeof confirmableActions> =
BaseModal & ConfirmOpts<T>;
export type PastDecisionsModal = BaseModal & PastDecisionsOpts;
export type Modal =
| BaseModal
| ConfirmModal<keyof typeof confirmableActions>
| PastDecisionsModal;

export interface ListUiItem {
isEditingName?: boolean;
editNameFormValue?: string;
Expand Down Expand Up @@ -489,15 +503,7 @@ interface BaseState {
host: string;
notifications: Notification[];
modals: {
[index: string]: {
action?:
| Action
| ThunkAction<Promise<void> | void, State, unknown, Action>;
actionLabel?: string;
body?: ReactNode;
restaurantId?: number;
shown: boolean;
};
[index: string]: Modal;
};
listUi: {
[index: number]: ListUiItem;
Expand Down
15 changes: 8 additions & 7 deletions src/routes/main/teams/Teams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,30 @@ import ListGroup from "react-bootstrap/ListGroup";
import { FaTimes } from "react-icons/fa";
import Container from "react-bootstrap/Container";
import Link from "../../../components/Link/Link";
import { ConfirmOpts, Team } from "../../../interfaces";
import { ConfirmOpts, Team, User } from "../../../interfaces";
import s from "./Teams.scss";

interface TeamsProps {
confirm: (props: ConfirmOpts) => void;
confirm: (props: ConfirmOpts<"removeUser">) => void;
host: string;
leaveTeam: (team: Team) => () => void;
teams: Team[];
user: User;
}

class Teams extends Component<TeamsProps> {
confirmLeave = (team: Team) => (event: MouseEvent) => {
confirmLeave = (user: User, team: Team) => (event: MouseEvent) => {
event.preventDefault();
this.props.confirm({
actionLabel: "Leave",
body: `Are you sure you want to leave this team?
You will need to be invited back by another member.`,
action: this.props.leaveTeam(team),
action: "removeUser",
actionArgs: [user.id, team],
});
};

render() {
const { host, teams } = this.props;
const { host, teams, user } = this.props;

return (
<div className={s.root}>
Expand All @@ -46,7 +47,7 @@ You will need to be invited back by another member.`,
<div className={s.itemName}>{team.name}</div>
<button
className={s.leave}
onClick={this.confirmLeave(team)}
onClick={this.confirmLeave(user, team)}
aria-label="Leave"
type="button"
>
Expand Down
17 changes: 4 additions & 13 deletions src/routes/main/teams/TeamsContainer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { connect } from "react-redux";
import { showModal } from "../../../actions/modals";
import { removeUser } from "../../../actions/users";
import { ConfirmOpts, Dispatch, State, Team } from "../../../interfaces";
import { ConfirmOpts, Dispatch, State } from "../../../interfaces";
import { getCurrentUser } from "../../../selectors/user";
import { getTeams } from "../../../selectors/teams";
import Teams from "./Teams";
Expand All @@ -13,17 +12,9 @@ const mapStateToProps = (state: State) => ({
});

const mapDispatchToProps = (dispatch: Dispatch) => ({
confirm: (opts: ConfirmOpts) => dispatch(showModal("confirm", opts)),
confirm: (opts: ConfirmOpts<"removeUser">) =>
dispatch(showModal("confirm", opts)),
dispatch,
});

const mergeProps = (
stateProps: ReturnType<typeof mapStateToProps>,
dispatchProps: ReturnType<typeof mapDispatchToProps>
) => ({
...stateProps,
...dispatchProps,
leaveTeam: (team: Team) => removeUser(stateProps.user!.id, team),
});

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Teams);
export default connect(mapStateToProps, mapDispatchToProps)(Teams);
14 changes: 8 additions & 6 deletions src/routes/team/team/Team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import {
Team as TeamType,
ConfirmOpts,
Action,
RoleType,
} from "../../../interfaces";
import s from "./Team.scss";

interface TeamProps {
changeTeamURLShown: boolean;
changeUserRole: (userId: number, role: string) => Action;
confirm: (options: ConfirmOpts) => void;
confirm: (options: ConfirmOpts<"changeUserRole">) => void;
confirmChangeTeamURL: () => void;
confirmDeleteTeam: () => void;
currentUser: User;
Expand All @@ -59,9 +60,7 @@ class Team extends React.Component<TeamProps> {
(user: User) => (event: ChangeEvent<HTMLSelectElement>) => {
const { currentUser, team } = this.props;

const newRole = event.target.value;

const changeRole = this.props.changeUserRole(user.id, newRole);
const newRole = event.target.value as RoleType;

if (
event.target.value === "member" &&
Expand All @@ -70,15 +69,18 @@ class Team extends React.Component<TeamProps> {
this.props.confirm({
actionLabel: "Promote",
body: "Are you sure you want to promote this user to Member status? You will not be able to demote them later.",
action: changeRole,
action: "changeUserRole",
actionArgs: [user.id, newRole],
});
} else if (currentUser.id === user.id && !currentUser.superuser) {
this.props.confirm({
actionLabel: "Demote",
body: "Are you sure you want to demote yourself? You will not be able to undo this by yourself.",
action: changeRole,
action: "changeUserRole",
actionArgs: [user.id, newRole],
});
} else {
const changeRole = this.props.changeUserRole(user.id, newRole);
this.props.dispatch(changeRole);
}
};
Expand Down
3 changes: 2 additions & 1 deletion src/routes/team/team/TeamContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const mapStateToProps = (state: State) => ({

const mapDispatchToProps = (dispatch: Dispatch) => ({
changeUserRole,
confirm: (opts: ConfirmOpts) => dispatch(showModal("confirm", opts)),
confirm: (opts: ConfirmOpts<"changeUserRole">) =>
dispatch(showModal("confirm", opts)),
confirmChangeTeamURL: () => dispatch(showModal("changeTeamURL")),
confirmDeleteTeam: () => dispatch(showModal("deleteTeam")),
dispatch,
Expand Down

0 comments on commit 37ab829

Please sign in to comment.