diff --git a/src/pages/settings/tabs/UserManagementSettings.tsx b/src/pages/settings/tabs/UserManagementSettings.tsx index 6589eeb3..24a4515b 100644 --- a/src/pages/settings/tabs/UserManagementSettings.tsx +++ b/src/pages/settings/tabs/UserManagementSettings.tsx @@ -2,8 +2,9 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { mdiCircleEditOutline, mdiLoading, mdiMagnify, mdiMinusCircleOutline } from '@mdi/js'; import { Icon } from '@mdi/react'; -import { cloneDeep, find, isEqual, remove } from 'lodash'; +import { find, isEqual, map, remove } from 'lodash'; import { useImmer } from 'use-immer'; +import { useToggle } from 'usehooks-ts'; import Button from '@/components/Input/Button'; import Checkbox from '@/components/Input/Checkbox'; @@ -23,53 +24,40 @@ import useEventCallback from '@/hooks/useEventCallback'; import type { UserType } from '@/core/types/api/user'; -const initialUser = { - ID: 0, - Username: '', - IsAdmin: true, - RestrictedTags: [], - CommunitySites: { - AniDB: true, - Trakt: false, - Plex: false, - }, - Avatar: '', - PlexUsernames: '', -} as UserType; - function UserManagementSettings() { const dispatch = useDispatch(); const currentUserQuery = useCurrentUserQuery(); const usersQuery = useUsersQuery(); - const users = useMemo(() => usersQuery.data ?? [], [usersQuery.data]); const { isPending: editUserPending, mutate: editUser } = usePutUserMutation(); const { mutate: deleteUser } = useDeleteUserMutation(); const { isPending: isChangePasswordPending, mutate: changePassword } = useChangePasswordMutation(); - const [selectedUser, setSelectedUser] = useImmer(initialUser); + + const [selectedUser, setSelectedUser] = useImmer(undefined); const [newPassword, setNewPassword] = useState(''); - const [logoutOthers, setLogoutOthers] = useState(false); + const [logoutOthers, toggleLogoutOthers, setLogoutOthers] = useToggle(false); const [tagSearch, setTagSearch] = useState(''); const [avatarFile, setAvatarFile] = useState(); - const [showAvatarModal, setShowAvatarModal] = useState(false); + const [showAvatarModal, toggleAvatarModal] = useToggle(false); const tagsQuery = useAniDBTagsQuery({ pageSize: 0, excludeDescriptions: true }); useEffect(() => { - if (users.length > 0) setSelectedUser(users[0]); - }, [users, setSelectedUser]); + if (!usersQuery.data || !!selectedUser) return; + setSelectedUser(usersQuery.data[0]); + }, [selectedUser, setSelectedUser, usersQuery.data]); const unsavedChanges = useMemo( () => { - if (!selectedUser.ID) return false; - const user = find(users, tempUser => tempUser.ID === selectedUser.ID); + if (!selectedUser?.ID) return false; + const user = find(usersQuery.data, tempUser => tempUser.ID === selectedUser.ID); return !isEqual(selectedUser, user); }, - [selectedUser, users], + [selectedUser, usersQuery.data], ); useEffect(() => { - if (!selectedUser.ID) return; + if (!selectedUser?.ID) return; if (!unsavedChanges) { toast.dismiss('unsaved'); } else { @@ -84,10 +72,12 @@ function UserManagementSettings() { const handleInputChange = (event: React.ChangeEvent) => { const { id } = event.target; const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; - const tempUser = cloneDeep(selectedUser); - if ((id === 'Trakt' || id === 'AniDB') && typeof value === 'boolean') tempUser.CommunitySites[id] = value; - else tempUser[id] = value; - setSelectedUser(tempUser); + + setSelectedUser((draftState) => { + if (!draftState) return; + if ((id === 'Trakt' || id === 'AniDB') && typeof value === 'boolean') draftState.CommunitySites[id] = value; + else draftState[id] = value; + }); }; useEffect(() => { @@ -103,7 +93,8 @@ function UserManagementSettings() { } }, [newPassword]); - const handlePasswordChange = () => { + const handlePasswordChange = useEventCallback(() => { + if (!selectedUser) return; changePassword({ Password: newPassword, RevokeAPIKeys: logoutOthers, @@ -119,13 +110,13 @@ function UserManagementSettings() { } else toast.success('Password changed successfully!'); }, }); - }; + }); - const handleCancel = () => { + const handleCancel = useEventCallback(() => { setNewPassword(''); setLogoutOthers(false); - setSelectedUser(find(users, user => user.ID === selectedUser.ID)!); - }; + setSelectedUser(find(usersQuery.data, user => user.ID === selectedUser?.ID)); + }); const openAvatarModal = (event: React.ChangeEvent) => { const avatar = event.target.files?.[0]; @@ -133,21 +124,17 @@ function UserManagementSettings() { event.target.value = ''; // This is a hack (yes, another) to make the onChange trigger even when same file is selected if (!avatar) return; setAvatarFile(avatar); - setShowAvatarModal(true); + toggleAvatarModal(); }; const changeAvatar = (avatar: string) => { - setSelectedUser((immerState) => { - immerState.Avatar = avatar; + setSelectedUser((draftState) => { + if (!draftState) return; + draftState.Avatar = avatar; }); }; - const removeAvatar = useEventCallback(() => { - // Setting the avatar to an empty string will tell the server to remove the avatar. - setSelectedUser((immerState) => { - immerState.Avatar = ''; - }); - }); + const removeAvatar = useEventCallback(() => changeAvatar('')); const deleteSelectedUser = (user: UserType) => { if (currentUserQuery.data?.ID === user.ID) { @@ -161,20 +148,30 @@ function UserManagementSettings() { }; const handleTagChange = (tagId: number, selected: boolean) => { - const tempUser = cloneDeep(selectedUser); - if (selected && !tempUser.RestrictedTags.find(tag => tag === tagId)) { - tempUser.RestrictedTags.push(tagId); - tempUser.RestrictedTags = tempUser.RestrictedTags.sort((tagA, tagB) => { + setSelectedUser((draftState) => { + if (!draftState) return; + + if (!selected) { + remove(draftState.RestrictedTags, tag => tag === tagId); + return; + } + + if (draftState.RestrictedTags.find(tag => tag === tagId)) return; + + draftState.RestrictedTags.push(tagId); + draftState.RestrictedTags = draftState.RestrictedTags.sort((tagA, tagB) => { const tagAName = tagsQuery.data?.find(tag => tag.ID === tagA)?.Name; const tagBName = tagsQuery.data?.find(tag => tag.ID === tagB)?.Name; if (tagAName === undefined || tagBName === undefined) return 0; return tagAName?.localeCompare(tagBName); }); - } - if (!selected) remove(tempUser.RestrictedTags, tag => tag === tagId); - setSelectedUser(tempUser); + }); }; + if (!selectedUser) { + return ; + } + return ( <>
@@ -190,7 +187,7 @@ function UserManagementSettings() {
Current Users
- {users.map(user => ( + {map(usersQuery.data, user => (
{user.Username}
@@ -286,7 +283,7 @@ function UserManagementSettings() {
Password
@@ -381,7 +378,7 @@ function UserManagementSettings() {
- +