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

Modernize redux: userDetailsSlice #243

Merged
merged 5 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 0 additions & 24 deletions app/src/actions/userDetailsActions.ts

This file was deleted.

2 changes: 1 addition & 1 deletion app/src/components/users/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import TableFilters from "../shared/TableFilters";
import Table from "../shared/Table";
import Notifications from "../shared/Notifications";
import NewResourceModal from "../shared/NewResourceModal";
import { usersTemplateMap } from "../../configs/tableConfigs/usersTableConfig";
import { usersTemplateMap } from "../../configs/tableConfigs/usersTableMap";
import { getTotalUsers } from "../../selectors/userSelectors";
import { fetchUsers } from "../../thunks/userThunks";
import {
Expand Down
9 changes: 4 additions & 5 deletions app/src/components/users/partials/UsersActionsCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { connect } from "react-redux";
import ConfirmModal from "../../shared/ConfirmModal";
import { deleteUser } from "../../../thunks/userThunks";
import UserDetailsModal from "./modal/UserDetailsModal";
import { fetchUserDetails } from "../../../thunks/userDetailsThunks";
import { getUserInformation } from "../../../selectors/userInfoSelectors";
import { hasAccess } from "../../../utils/utils";
import { useAppDispatch } from "../../../store";
import { fetchUserDetails } from "../../../slices/userDetailsSlice";

/**
* This component renders the action cells of users in the table view
*/
const UsersActionCell = ({
row,
deleteUser,
fetchUserDetails,
user
}: any) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const [displayDeleteConfirmation, setDeleteConfirmation] = useState(false);
const [displayUserDetails, setUserDetails] = useState(false);
Expand All @@ -32,7 +33,7 @@ const UsersActionCell = ({
};

const showUserDetails = async () => {
await fetchUserDetails(row.username);
await dispatch(fetchUserDetails(row.username));

setUserDetails(true);
};
Expand Down Expand Up @@ -93,8 +94,6 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => ({
// @ts-expect-error TS(7006): Parameter 'id' implicitly has an 'any' type.
deleteUser: (id) => dispatch(deleteUser(id)),
// @ts-expect-error TS(7006): Parameter 'username' implicitly has an 'any' type.
fetchUserDetails: (username) => dispatch(fetchUserDetails(username)),
});

export default connect(mapStateToProps, mapDispatchToProps)(UsersActionCell);
35 changes: 12 additions & 23 deletions app/src/components/users/partials/modal/UserDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@ import { Formik } from "formik";
import cn from "classnames";
import { EditUserSchema } from "../../../../utils/validate";
import UserRolesTab from "../wizard/UserRolesTab";
import { connect } from "react-redux";
import { getUserDetails } from "../../../../selectors/userDetailsSelectors";
import EditUserGeneralTab from "../wizard/EditUserGeneralTab";
import UserEffectiveRolesTab from "../wizard/UserEffectiveRolesTab";
import { updateUserDetails } from "../../../../thunks/userDetailsThunks";
import ModalNavigation from "../../../shared/modals/ModalNavigation";
import { useAppDispatch, useAppSelector } from "../../../../store";
import { updateUserDetails } from "../../../../slices/userDetailsSlice";

/**
* This component manages the pages of the user details
*/
const UserDetails = ({
close,
userDetails,
updateUserDetails
}: any) => {
const UserDetails: React.FC<{
close: () => void
}> = ({
close,
}) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();

const [page, setPage] = useState(0);

const userDetails = useAppSelector(state => getUserDetails(state));

const initialValues = {
...userDetails,
password: "",
Expand Down Expand Up @@ -54,7 +57,7 @@ const UserDetails = ({

// @ts-expect-error TS(7006): Parameter 'values' implicitly has an 'any' type.
const handleSubmit = (values) => {
updateUserDetails(values, userDetails.username);
dispatch(updateUserDetails({values: values, username: userDetails.username}));
close();
};

Expand Down Expand Up @@ -101,18 +104,4 @@ const UserDetails = ({
);
};

// Getting state data out of redux store
// @ts-expect-error TS(7006): Parameter 'state' implicitly has an 'any' type.
const mapStateToProps = (state) => ({
userDetails: getUserDetails(state),
});

// Mapping actions to dispatch
// @ts-expect-error TS(7006): Parameter 'dispatch' implicitly has an 'any' type.
const mapDispatchToProps = (dispatch) => ({
// @ts-expect-error TS(7006): Parameter 'values' implicitly has an 'any' type.
updateUserDetails: (values, username) =>
dispatch(updateUserDetails(values, username)),
});

export default connect(mapStateToProps, mapDispatchToProps)(UserDetails);
export default UserDetails;
12 changes: 0 additions & 12 deletions app/src/configs/tableConfigs/usersTableConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import UsersActionCell from "../../components/users/partials/UsersActionsCell";
import UsersRolesCell from "../../components/users/partials/UsersRolesCell";

/**
* Config that contains the columns and further information regarding users. These are the information that never or hardly changes.
* That's why it is hard coded here and not fetched from server.
Expand Down Expand Up @@ -51,12 +48,3 @@ export const usersTableConfig = {
multiSelect: false,
};

/**
* This map contains the mapping between the template strings above and the corresponding react component.
* This helps to render different templates of cells more dynamically. Even empty needed, because Table component
* uses template map.
*/
export const usersTemplateMap = {
UsersActionsCell: UsersActionCell,
UsersRolesCell: UsersRolesCell,
};
12 changes: 12 additions & 0 deletions app/src/configs/tableConfigs/usersTableMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import UsersActionCell from "../../components/users/partials/UsersActionsCell";
import UsersRolesCell from "../../components/users/partials/UsersRolesCell";

/**
* This map contains the mapping between the template strings above and the corresponding react component.
* This helps to render different templates of cells more dynamically. Even empty needed, because Table component
* uses template map.
*/
export const usersTemplateMap = {
UsersActionsCell: UsersActionCell,
UsersRolesCell: UsersRolesCell,
};
62 changes: 0 additions & 62 deletions app/src/reducers/userDetailsReducer.ts

This file was deleted.

4 changes: 3 additions & 1 deletion app/src/selectors/userDetailsSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { RootState } from "../store";

/**
* This file contains selectors regarding details of a certain user
*/
export const getUserDetails = (state: any) => state.userDetails;
export const getUserDetails = (state: RootState) => state.userDetails;
104 changes: 104 additions & 0 deletions app/src/slices/userDetailsSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { PayloadAction, SerializedError, createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import axios from 'axios';
import { addNotification } from "../thunks/notificationThunks";
import { buildUserBody } from "../utils/resourceUtils";

/**
* This file contains redux reducer for actions affecting the state of details of a user
*/
export type UserDetailsState = {
status: 'uninitialized' | 'loading' | 'succeeded' | 'failed',
error: SerializedError | null,
provider: string,
roles: string[],
name: string,
username: string,
email: string,
manageable: boolean,
};

// Initial state of userDetails in redux store
const initialState: UserDetailsState = {
status: 'uninitialized',
error: null,
provider: "",
roles: [],
name: "",
username: "",
email: "",
manageable: false,
};

// fetch details about certain user from server
export const fetchUserDetails = createAsyncThunk('userDetails/fetchUserDetails', async (username: any) => {
// Just make the async request here, and return the response.
// This will automatically dispatch a `pending` action first,
// and then `fulfilled` or `rejected` actions based on the promise.
const res = await axios.get(`/admin-ng/users/${username}.json`);
return res.data;
});

// update existing user with changed values
export const updateUserDetails = createAsyncThunk('userDetails/updateUserDetails', async (params: {values: any, username: string}, {dispatch}) => {
const { username, values } = params

// get URL params used for put request
let data = buildUserBody(values);

// PUT request
axios
.put(`/admin-ng/users/${username}.json`, data)
.then((response) => {
console.info(response);
dispatch(addNotification("success", "USER_UPDATED"));
})
.catch((response) => {
console.error(response);
dispatch(addNotification("error", "USER_NOT_SAVED"));
});
});

const userDetailsSlice = createSlice({
name: 'userDetails',
initialState,
reducers: {},
// These are used for thunks
extraReducers: builder => {
builder
.addCase(fetchUserDetails.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUserDetails.fulfilled, (state, action: PayloadAction<{
provider: UserDetailsState["provider"],
roles: UserDetailsState["roles"],
name: UserDetailsState["name"],
username: UserDetailsState["username"],
email: UserDetailsState["email"],
manageable: UserDetailsState["manageable"],
}>) => {
state.status = 'succeeded';
const userDetails = action.payload;
state.provider = userDetails.provider;
state.roles = userDetails.roles;
state.name = userDetails.name;
state.username = userDetails.username;
state.email = !!userDetails.email ? userDetails.email : "";
state.manageable = userDetails.manageable;
})
.addCase(fetchUserDetails.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error;
state.provider = "";
state.roles = [];
state.name = "";
state.username = "";
state.email = "";
state.manageable = false;
});
}
});

// export const {} = userDetailsSlice.actions;

// Export the slice reducer as the default export
export default userDetailsSlice.reducer;
2 changes: 1 addition & 1 deletion app/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { notifications } from "./reducers/notificationReducers";
import workflows from "./slices/workflowSlice";
import eventDetails from "./reducers/eventDeatilsReducers";
import seriesDetails from "./reducers/seriesDetailsReducers";
import userDetails from "./slices/userDetailsSlice";
import recordingDetails from "./slices/recordingDetailsSlice";
import userDetails from "./reducers/userDetailsReducer";
import groupDetails from "./slices/groupDetailsSlice";
import aclDetails from "./reducers/aclDetailsReducer";
import themeDetails from "./reducers/themeDetailsReducer";
Expand Down
Loading
Loading