diff --git a/src/apps/dashboard/features/users/api/useAuthProviders.ts b/src/apps/dashboard/features/users/api/useAuthProviders.ts new file mode 100644 index 0000000000..f09a6d496a --- /dev/null +++ b/src/apps/dashboard/features/users/api/useAuthProviders.ts @@ -0,0 +1,25 @@ +import { Api } from '@jellyfin/sdk'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api'; + +const fetchAuthProviders = async (api?: Api) => { + if (!api) { + console.error('[useAuthProvider] No Api instance available'); + return; + } + + const response = await getSessionApi(api).getAuthProviders(); + + return response.data; +}; + +export const useAuthProviders = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'AuthProviders' ], + queryFn: () => fetchAuthProviders(api), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/users/api/useChannels.ts b/src/apps/dashboard/features/users/api/useChannels.ts new file mode 100644 index 0000000000..da6508ac71 --- /dev/null +++ b/src/apps/dashboard/features/users/api/useChannels.ts @@ -0,0 +1,26 @@ +import { Api } from '@jellyfin/sdk'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { getChannelsApi } from '@jellyfin/sdk/lib/utils/api/channels-api'; +import { ChannelsApiGetChannelsRequest } from '@jellyfin/sdk/lib/generated-client/api/channels-api'; + +const fetchChannels = async (api?: Api, params?: ChannelsApiGetChannelsRequest) => { + if (!api) { + console.error('[useAuthProvider] No Api instance available'); + return; + } + + const response = await getChannelsApi(api).getChannels(params); + + return response.data; +}; + +export const useChannels = (params?: ChannelsApiGetChannelsRequest) => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'Channels' ], + queryFn: () => fetchChannels(api, params), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/users/api/useCreateUser.ts b/src/apps/dashboard/features/users/api/useCreateUser.ts new file mode 100644 index 0000000000..fe935739d5 --- /dev/null +++ b/src/apps/dashboard/features/users/api/useCreateUser.ts @@ -0,0 +1,16 @@ +import { UserApiCreateUserByNameRequest } from '@jellyfin/sdk/lib/generated-client'; +import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api'; +import { useMutation } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; + +export const useCreateUser = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: UserApiCreateUserByNameRequest) => ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getUserApi(api!) + .createUserByName(params) + ) + }); +}; diff --git a/src/apps/dashboard/features/users/api/useDeleteUser.ts b/src/apps/dashboard/features/users/api/useDeleteUser.ts new file mode 100644 index 0000000000..5ab9bb0f37 --- /dev/null +++ b/src/apps/dashboard/features/users/api/useDeleteUser.ts @@ -0,0 +1,23 @@ +import { UserApiDeleteUserRequest } from '@jellyfin/sdk/lib/generated-client'; +import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api'; +import { useMutation } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { QUERY_KEY } from 'hooks/useUsers'; +import { queryClient } from 'utils/query/queryClient'; + +export const useDeleteUser = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: UserApiDeleteUserRequest) => ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getUserApi(api!) + .deleteUser(params) + ), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY ] + }); + } + }); +}; diff --git a/src/apps/dashboard/features/users/api/useLibraryMediaFolders.ts b/src/apps/dashboard/features/users/api/useLibraryMediaFolders.ts new file mode 100644 index 0000000000..ce028fb11f --- /dev/null +++ b/src/apps/dashboard/features/users/api/useLibraryMediaFolders.ts @@ -0,0 +1,26 @@ +import { Api } from '@jellyfin/sdk'; +import { LibraryApiGetMediaFoldersRequest } from '@jellyfin/sdk/lib/generated-client/api/library-api'; +import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; + +const fetchLibraryMediaFolders = async (api?: Api, params?: LibraryApiGetMediaFoldersRequest) => { + if (!api) { + console.error('[useLibraryMediaFolders] no Api instance available'); + return; + } + + const response = await getLibraryApi(api).getMediaFolders(params); + + return response.data; +}; + +export const useLibraryMediaFolders = (params?: LibraryApiGetMediaFoldersRequest) => { + const { api } = useApi(); + + return useQuery({ + queryKey: ['LibraryMediaFolders'], + queryFn: () => fetchLibraryMediaFolders(api, params), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/users/api/useNetworkConfig.ts b/src/apps/dashboard/features/users/api/useNetworkConfig.ts new file mode 100644 index 0000000000..24ab234f71 --- /dev/null +++ b/src/apps/dashboard/features/users/api/useNetworkConfig.ts @@ -0,0 +1,26 @@ +import { Api } from '@jellyfin/sdk'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; +import { NetworkConfiguration } from '@jellyfin/sdk/lib/generated-client'; + +const fetchNetworkConfig = async (api?: Api) => { + if (!api) { + console.error('[useAuthProvider] No Api instance available'); + return; + } + + const response = await getConfigurationApi(api).getNamedConfiguration({ key: 'network' }); + + return response.data as NetworkConfiguration; +}; + +export const useNetworkConfig = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'NetConfig' ], + queryFn: () => fetchNetworkConfig(api), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/users/api/useParentalRatings.ts b/src/apps/dashboard/features/users/api/useParentalRatings.ts new file mode 100644 index 0000000000..426b3eed18 --- /dev/null +++ b/src/apps/dashboard/features/users/api/useParentalRatings.ts @@ -0,0 +1,25 @@ +import { Api } from '@jellyfin/sdk'; +import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; + +const fetchParentalRatings = async (api?: Api) => { + if (!api) { + console.error('[useLibraryMediaFolders] no Api instance available'); + return; + } + + const response = await getLocalizationApi(api).getParentalRatings(); + + return response.data; +}; + +export const useParentalRatings = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: ['ParentalRatings'], + queryFn: () => fetchParentalRatings(api), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/users/api/usePasswordResetProviders.ts b/src/apps/dashboard/features/users/api/usePasswordResetProviders.ts new file mode 100644 index 0000000000..1da7784b5f --- /dev/null +++ b/src/apps/dashboard/features/users/api/usePasswordResetProviders.ts @@ -0,0 +1,25 @@ +import { Api } from '@jellyfin/sdk'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api'; + +const fetchPasswordResetProviders = async (api?: Api) => { + if (!api) { + console.error('[useAuthProvider] No Api instance available'); + return; + } + + const response = await getSessionApi(api).getPasswordResetProviders(); + + return response.data; +}; + +export const usePasswordResetProviders = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'PasswordResetProviders' ], + queryFn: () => fetchPasswordResetProviders(api), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/users/api/useUpdateUser.ts b/src/apps/dashboard/features/users/api/useUpdateUser.ts new file mode 100644 index 0000000000..93a0133e97 --- /dev/null +++ b/src/apps/dashboard/features/users/api/useUpdateUser.ts @@ -0,0 +1,23 @@ +import { UserApiUpdateUserRequest } from '@jellyfin/sdk/lib/generated-client'; +import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api'; +import { useMutation } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { queryClient } from 'utils/query/queryClient'; +import { QUERY_KEY } from './useUser'; + +export const useUpdateUser = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: UserApiUpdateUserRequest) => ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getUserApi(api!) + .updateUser(params) + ), + onSuccess: (_, params) => { + void queryClient.invalidateQueries({ + queryKey: [QUERY_KEY, params.userId] + }); + } + }); +}; diff --git a/src/apps/dashboard/features/users/api/useUpdateUserPolicy.ts b/src/apps/dashboard/features/users/api/useUpdateUserPolicy.ts new file mode 100644 index 0000000000..f7077609ca --- /dev/null +++ b/src/apps/dashboard/features/users/api/useUpdateUserPolicy.ts @@ -0,0 +1,23 @@ +import { UserApiUpdateUserPolicyRequest } from '@jellyfin/sdk/lib/generated-client'; +import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api'; +import { useMutation } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { queryClient } from 'utils/query/queryClient'; +import { QUERY_KEY } from './useUser'; + +export const useUpdateUserPolicy = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: UserApiUpdateUserPolicyRequest) => ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getUserApi(api!) + .updateUserPolicy(params) + ), + onSuccess: (_, params) => { + void queryClient.invalidateQueries({ + queryKey: [QUERY_KEY, params.userId] + }); + } + }); +}; diff --git a/src/apps/dashboard/features/users/api/useUser.ts b/src/apps/dashboard/features/users/api/useUser.ts new file mode 100644 index 0000000000..6884c596bc --- /dev/null +++ b/src/apps/dashboard/features/users/api/useUser.ts @@ -0,0 +1,33 @@ +import { Api } from '@jellyfin/sdk'; +import { UserApiGetUserByIdRequest } from '@jellyfin/sdk/lib/generated-client'; +import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; + +export const QUERY_KEY = 'User'; + +const fetchUser = async (api?: Api, params?: UserApiGetUserByIdRequest) => { + if (!api) { + console.error('[useUser] No Api instance available'); + return; + } + + if (!params) { + console.error('[useUser] Missing request params'); + return; + } + + const response = await getUserApi(api).getUserById(params); + + return response.data; +}; + +export const useUser = (params?: UserApiGetUserByIdRequest) => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ QUERY_KEY, params?.userId ], + queryFn: () => fetchUser(api, params), + enabled: !!api && !!params + }); +}; diff --git a/src/apps/dashboard/routes/users/add.tsx b/src/apps/dashboard/routes/users/add.tsx index ad66aa61f6..fcf0716920 100644 --- a/src/apps/dashboard/routes/users/add.tsx +++ b/src/apps/dashboard/routes/users/add.tsx @@ -1,4 +1,4 @@ -import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import type { BaseItemDto, CreateUserByName } from '@jellyfin/sdk/lib/generated-client'; import React, { useCallback, useEffect, useState, useRef } from 'react'; import Dashboard from '../../../../utils/dashboard'; @@ -12,10 +12,10 @@ import CheckBoxElement from '../../../../elements/CheckBoxElement'; import Page from '../../../../components/Page'; import Toast from 'apps/dashboard/components/Toast'; -type UserInput = { - Name?: string; - Password?: string; -}; +import { useLibraryMediaFolders } from 'apps/dashboard/features/users/api/useLibraryMediaFolders'; +import { useChannels } from 'apps/dashboard/features/users/api/useChannels'; +import { useUpdateUserPolicy } from 'apps/dashboard/features/users/api/useUpdateUserPolicy'; +import { useCreateUser } from 'apps/dashboard/features/users/api/useCreateUser'; type ItemsArr = { Name?: string | null; @@ -31,6 +31,11 @@ const UserNew = () => { const handleToastClose = useCallback(() => { setIsErrorToastOpen(false); }, []); + const { data: mediaFolders, isSuccess: isMediaFoldersSuccess } = useLibraryMediaFolders(); + const { data: channels, isSuccess: isChannelsSuccess } = useChannels(); + + const createUser = useCreateUser(); + const updateUserPolicy = useUpdateUserPolicy(); const getItemsResult = (items: BaseItemDto[]) => { return items.map(item => @@ -49,9 +54,7 @@ const UserNew = () => { return; } - const mediaFolders = getItemsResult(result); - - setMediaFoldersItems(mediaFolders); + setMediaFoldersItems(getItemsResult(result)); const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement; folderAccess.dispatchEvent(new CustomEvent('create')); @@ -67,15 +70,15 @@ const UserNew = () => { return; } - const channels = getItemsResult(result); + const channelItems = getItemsResult(result); - setChannelsItems(channels); + setChannelsItems(channelItems); const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement; channelAccess.dispatchEvent(new CustomEvent('create')); const channelAccessContainer = page.querySelector('.channelAccessContainer') as HTMLDivElement; - channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide'); + channelItems.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide'); (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false; }, []); @@ -87,22 +90,26 @@ const UserNew = () => { console.error('Unexpected null reference'); return; } + if (!mediaFolders?.Items) { + console.error('[add] mediaFolders not available'); + return; + } + if (!channels?.Items) { + console.error('[add] channels not available'); + return; + } - (page.querySelector('#txtUsername') as HTMLInputElement).value = ''; - (page.querySelector('#txtPassword') as HTMLInputElement).value = ''; + loadMediaFolders(mediaFolders?.Items); + loadChannels(channels?.Items); + loading.hide(); + }, [loadChannels, loadMediaFolders, mediaFolders, channels]); + + useEffect(() => { loading.show(); - const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { - IsHidden: false - })); - const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels')); - Promise.all([promiseFolders, promiseChannels]).then(function (responses) { - loadMediaFolders(responses[0].Items); - loadChannels(responses[1].Items); - loading.hide(); - }).catch(err => { - console.error('[usernew] failed to load data', err); - }); - }, [loadChannels, loadMediaFolders]); + if (isMediaFoldersSuccess && isChannelsSuccess) { + loadUser(); + } + }, [loadUser, isMediaFoldersSuccess, isChannelsSuccess]); useEffect(() => { const page = element.current; @@ -112,51 +119,57 @@ const UserNew = () => { return; } - loadUser(); - const saveUser = () => { - const userInput: UserInput = {}; - userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value.trim(); - userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value; + const userInput: CreateUserByName = { + Name: (page.querySelector('#txtUsername') as HTMLInputElement).value, + Password: (page.querySelector('#txtPassword') as HTMLInputElement).value + }; + createUser.mutate({ createUserByName: userInput }, { + onSuccess: (response) => { + const user = response.data; - window.ApiClient.createUser(userInput).then(function (user) { - if (!user.Id || !user.Policy) { - throw new Error('Unexpected null user id or policy'); - } + if (!user.Id || !user.Policy) { + throw new Error('Unexpected null user id or policy'); + } - user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked; - user.Policy.EnabledFolders = []; + user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked; + user.Policy.EnabledFolders = []; - if (!user.Policy.EnableAllFolders) { - user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) { - return i.checked; - }).map(function (i) { - return i.getAttribute('data-id'); - }); - } - - user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked; - user.Policy.EnabledChannels = []; - - if (!user.Policy.EnableAllChannels) { - user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) { - return i.checked; - }).map(function (i) { - return i.getAttribute('data-id'); - }); - } - - window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () { - Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id) - .catch(err => { - console.error('[usernew] failed to navigate to edit user page', err); + if (!user.Policy.EnableAllFolders) { + user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) { + return i.checked; + }).map(function (i) { + return i.getAttribute('data-id'); }); - }).catch(err => { - console.error('[usernew] failed to update user policy', err); - }); - }, function () { - setIsErrorToastOpen(true); - loading.hide(); + } + + user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked; + user.Policy.EnabledChannels = []; + + if (!user.Policy.EnableAllChannels) { + user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) { + return i.checked; + }).map(function (i) { + return i.getAttribute('data-id'); + }); + } + + updateUserPolicy.mutate({ + userId: user.Id, + userPolicy: user.Policy + }, { + onSuccess: () => { + Dashboard.navigate('/dashboard/users/profile?userId=' + user.Id) + .catch(err => { + console.error('[usernew] failed to navigate to edit user page', err); + }); + }, + onError: () => { + console.error('[usernew] failed to update user policy'); + setIsErrorToastOpen(true); + } + }); + } }); }; @@ -168,22 +181,32 @@ const UserNew = () => { return false; }; - (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { + const enableAllChannelsChange = function (this: HTMLInputElement) { const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement; this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide'); - }); + }; - (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { + const enableAllFoldersChange = function (this: HTMLInputElement) { const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement; this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide'); - }); + }; - (page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); - - (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() { + const onCancelClick = () => { window.history.back(); - }); - }, [loadUser]); + }; + + (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', enableAllChannelsChange); + (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', enableAllFoldersChange); + (page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); + (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', onCancelClick); + + return () => { + (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).removeEventListener('change', enableAllChannelsChange); + (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).removeEventListener('change', enableAllFoldersChange); + (page.querySelector('.newUserProfileForm') as HTMLFormElement).removeEventListener('submit', onSubmit); + (page.querySelector('#btnCancel') as HTMLButtonElement).removeEventListener('click', onCancelClick); + }; + }, [loadUser, createUser, updateUserPolicy]); return ( { const location = useLocation(); const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false); - const [ users, setUsers ] = useState([]); - const element = useRef(null); + const { data: users, isPending } = useUsers(); + const deleteUser = useDeleteUser(); const handleToastClose = useCallback(() => { setIsSettingsSavedToastOpen(false); }, []); - const loadData = () => { - loading.show(); - window.ApiClient.getUsers().then(function (result) { - setUsers(result); - loading.hide(); - }).catch(err => { - console.error('[userprofiles] failed to fetch users', err); - }); - }; - useEffect(() => { const page = element.current; @@ -57,8 +47,6 @@ const UserProfiles = () => { return; } - loadData(); - const showUserMenu = (elem: HTMLElement) => { const card = dom.parentWithClass(elem, 'card'); const userId = card?.getAttribute('data-userid'); @@ -120,7 +108,7 @@ const UserProfiles = () => { break; case 'delete': - deleteUser(userId, username); + confirmDeleteUser(userId, username); } } }).catch(() => { @@ -131,7 +119,7 @@ const UserProfiles = () => { }); }; - const deleteUser = (id: string, username?: string | null) => { + const confirmDeleteUser = (id: string, username?: string | null) => { const title = username ? globalize.translate('DeleteName', username) : globalize.translate('DeleteUser'); const text = globalize.translate('DeleteUserConfirmation'); @@ -141,32 +129,41 @@ const UserProfiles = () => { confirmText: globalize.translate('Delete'), primary: 'delete' }).then(function () { - loading.show(); - window.ApiClient.deleteUser(id).then(function () { - loadData(); - }).catch(err => { - console.error('[userprofiles] failed to delete user', err); + deleteUser.mutate({ + userId: id }); }).catch(() => { // confirm dialog closed }); }; - page.addEventListener('click', function (e) { + const onPageClick = function (e: MouseEvent) { const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu'); if (btnUserMenu) { showUserMenu(btnUserMenu); } - }); + }; - (page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() { + const onAddUserClick = function() { Dashboard.navigate('/dashboard/users/add') .catch(err => { console.error('[userprofiles] failed to navigate to new user page', err); }); - }); - }, []); + }; + + page.addEventListener('click', onPageClick); + (page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', onAddUserClick); + + return () => { + page.removeEventListener('click', onPageClick); + (page.querySelector('#btnAddUser') as HTMLButtonElement).removeEventListener('click', onAddUserClick); + }; + }, [deleteUser]); + + if (isPending) { + return ; + } return ( {
- {users.map(user => { + {users?.map(user => { return ; })}
diff --git a/src/apps/dashboard/routes/users/password.tsx b/src/apps/dashboard/routes/users/password.tsx index 41f1fb4529..36bfa9db5c 100644 --- a/src/apps/dashboard/routes/users/password.tsx +++ b/src/apps/dashboard/routes/users/password.tsx @@ -1,37 +1,21 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React from 'react'; import { useSearchParams } from 'react-router-dom'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import Page from '../../../../components/Page'; -import loading from '../../../../components/loading/loading'; +import { useUser } from 'apps/dashboard/features/users/api/useUser'; +import Loading from 'components/loading/LoadingComponent'; const UserPassword = () => { const [ searchParams ] = useSearchParams(); const userId = searchParams.get('userId'); - const [ userName, setUserName ] = useState(''); + const { data: user, isPending } = useUser(userId ? { userId: userId } : undefined); - const loadUser = useCallback(() => { - if (!userId) { - console.error('[userpassword] missing user id'); - return; - } - - loading.show(); - window.ApiClient.getUser(userId).then(function (user) { - if (!user.Name) { - throw new Error('Unexpected null user.Name'); - } - setUserName(user.Name); - loading.hide(); - }).catch(err => { - console.error('[userpassword] failed to fetch user', err); - }); - }, [userId]); - useEffect(() => { - loadUser(); - }, [loadUser]); + if (isPending || !user) { + return ; + } return ( {
diff --git a/src/apps/dashboard/routes/users/profile.tsx b/src/apps/dashboard/routes/users/profile.tsx index 782f70e619..35ec1831e5 100644 --- a/src/apps/dashboard/routes/users/profile.tsx +++ b/src/apps/dashboard/routes/users/profile.tsx @@ -13,6 +13,14 @@ import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import loading from '../../../../components/loading/loading'; import SelectElement from '../../../../elements/SelectElement'; import Page from '../../../../components/Page'; +import { useUser } from 'apps/dashboard/features/users/api/useUser'; +import { useAuthProviders } from 'apps/dashboard/features/users/api/useAuthProviders'; +import { usePasswordResetProviders } from 'apps/dashboard/features/users/api/usePasswordResetProviders'; +import { useLibraryMediaFolders } from 'apps/dashboard/features/users/api/useLibraryMediaFolders'; +import { useChannels } from 'apps/dashboard/features/users/api/useChannels'; +import { useUpdateUser } from 'apps/dashboard/features/users/api/useUpdateUser'; +import { useUpdateUserPolicy } from 'apps/dashboard/features/users/api/useUpdateUserPolicy'; +import { useNetworkConfig } from 'apps/dashboard/features/users/api/useNetworkConfig'; type ResetProvider = BaseItemDto & { checkedAttribute: string @@ -27,15 +35,22 @@ const UserEdit = () => { const navigate = useNavigate(); const [ searchParams ] = useSearchParams(); const userId = searchParams.get('userId'); - const [ userDto, setUserDto ] = useState(); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]); - const [ authProviders, setAuthProviders ] = useState([]); - const [ passwordResetProviders, setPasswordResetProviders ] = useState([]); const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); const [ authenticationProviderId, setAuthenticationProviderId ] = useState(''); const [ passwordResetProviderId, setPasswordResetProviderId ] = useState(''); + const { data: userDto, isSuccess: isUserSuccess } = useUser(userId ? { userId: userId } : undefined); + const { data: authProviders, isSuccess: isAuthProvidersSuccess } = useAuthProviders(); + const { data: passwordResetProviders, isSuccess: isPasswordResetProvidersSuccess } = usePasswordResetProviders(); + const { data: mediaFolders, isSuccess: isMediaFoldersSuccess } = useLibraryMediaFolders({ isHidden: false }); + const { data: channels, isSuccess: isChannelsSuccess } = useChannels({ supportsMediaDeletion: true }); + const { data: netConfig, isSuccess: isNetConfigSuccess } = useNetworkConfig(); + + const updateUser = useUpdateUser(); + const updateUserPolicy = useUpdateUserPolicy(); + const element = useRef(null); const triggerChange = (select: HTMLInputElement) => { @@ -43,17 +58,10 @@ const UserEdit = () => { select.dispatchEvent(evt); }; - const getUser = () => { - if (!userId) throw new Error('missing user id'); - return window.ApiClient.getUser(userId); - }; - const loadAuthProviders = useCallback((page: HTMLDivElement, user: UserDto, providers: NameIdPair[]) => { const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement; fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1); - setAuthProviders(providers); - const currentProviderId = user.Policy?.AuthenticationProviderId || ''; setAuthenticationProviderId(currentProviderId); }, []); @@ -62,30 +70,26 @@ const UserEdit = () => { const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement; fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1); - setPasswordResetProviders(providers); - const currentProviderId = user.Policy?.PasswordResetProviderId || ''; setPasswordResetProviderId(currentProviderId); }, []); - const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, mediaFolders: BaseItemDto[]) => { - window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', { - SupportsMediaDeletion: true - })).then(function (channelsResult) { - let isChecked; - let checkedAttribute; - const itemsArr: ResetProvider[] = []; + const loadDeleteFolders = useCallback((page: HTMLDivElement, user: UserDto, folders: BaseItemDto[]) => { + let isChecked; + let checkedAttribute; + const itemsArr: ResetProvider[] = []; - for (const mediaFolder of mediaFolders) { - isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1; - checkedAttribute = isChecked ? ' checked="checked"' : ''; - itemsArr.push({ - ...mediaFolder, - checkedAttribute: checkedAttribute - }); - } + for (const mediaFolder of folders) { + isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(mediaFolder.Id || '') != -1; + checkedAttribute = isChecked ? ' checked="checked"' : ''; + itemsArr.push({ + ...mediaFolder, + checkedAttribute: checkedAttribute + }); + } - for (const channel of channelsResult.Items) { + if (channels?.Items) { + for (const channel of channels.Items) { isChecked = user.Policy?.EnableContentDeletion || user.Policy?.EnableContentDeletionFromFolders?.indexOf(channel.Id || '') != -1; checkedAttribute = isChecked ? ' checked="checked"' : ''; itemsArr.push({ @@ -93,16 +97,66 @@ const UserEdit = () => { checkedAttribute: checkedAttribute }); } + } - setDeleteFoldersAccess(itemsArr); + setDeleteFoldersAccess(itemsArr); - const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement; - chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false; - triggerChange(chkEnableDeleteAllFolders); - }).catch(err => { - console.error('[useredit] failed to fetch channels', err); - }); - }, []); + const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement; + chkEnableDeleteAllFolders.checked = user.Policy?.EnableContentDeletion || false; + triggerChange(chkEnableDeleteAllFolders); + }, [channels]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('[useredit] Unexpected null page reference'); + return; + } + + if (userDto && isAuthProvidersSuccess && authProviders != null) { + loadAuthProviders(page, userDto, authProviders); + } + }, [authProviders, isAuthProvidersSuccess, userDto, loadAuthProviders]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('[useredit] Unexpected null page reference'); + return; + } + + if (userDto && isPasswordResetProvidersSuccess && passwordResetProviders != null) { + loadPasswordResetProviders(page, userDto, passwordResetProviders); + } + }, [passwordResetProviders, isPasswordResetProvidersSuccess, userDto, loadPasswordResetProviders]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('[useredit] Unexpected null page reference'); + return; + } + + if (userDto && isMediaFoldersSuccess && isChannelsSuccess && mediaFolders?.Items != null) { + loadDeleteFolders(page, userDto, mediaFolders.Items); + } + }, [userDto, mediaFolders, isMediaFoldersSuccess, isChannelsSuccess, channels, loadDeleteFolders]); + + useEffect(() => { + const page = element.current; + + if (!page) { + console.error('[useredit] Unexpected null page reference'); + return; + } + + if (netConfig && isNetConfigSuccess) { + (page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !netConfig.EnableRemoteAccess); + } + }, [netConfig, isNetConfigSuccess]); const loadUser = useCallback((user: UserDto) => { const page = element.current; @@ -112,24 +166,6 @@ const UserEdit = () => { return; } - window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) { - loadAuthProviders(page, user, providers); - }).catch(err => { - console.error('[useredit] failed to fetch auth providers', err); - }); - window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) { - loadPasswordResetProviders(page, user, providers); - }).catch(err => { - console.error('[useredit] failed to fetch password reset providers', err); - }); - window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', { - IsHidden: false - })).then(function (folders) { - loadDeleteFolders(page, user, folders.Items); - }).catch(err => { - console.error('[useredit] failed to fetch media folders', err); - }); - const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement; disabledUserBanner.classList.toggle('hide', !user.Policy?.IsDisabled); @@ -139,7 +175,6 @@ const UserEdit = () => { void libraryMenu.then(menu => menu.setTitle(user.Name)); - setUserDto(user); (page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || ''; (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator; (page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled; @@ -163,16 +198,22 @@ const UserEdit = () => { (page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(user.Policy?.MaxActiveSessions) || '0'; (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(user.Policy?.SyncPlayAccess); loading.hide(); - }, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]); + }, [ userDto, loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]); const loadData = useCallback(() => { + if (!userDto) { + console.error('[profile] No user available'); + return; + } loading.show(); - getUser().then(function (user) { - loadUser(user); - }).catch(err => { - console.error('[useredit] failed to load data', err); - }); - }, [loadUser]); + loadUser(userDto); + }, [userDto, loadUser]); + + useEffect(() => { + if (isUserSuccess) { + loadData(); + } + }, [loadData, isUserSuccess]); useEffect(() => { const page = element.current; @@ -182,8 +223,6 @@ const UserEdit = () => { return; } - loadData(); - const saveUser = (user: UserDto) => { if (!user.Id || !user.Policy) { throw new Error('Unexpected null user id or policy'); @@ -215,53 +254,57 @@ const UserEdit = () => { user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder')); user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType; - window.ApiClient.updateUser(user).then(() => ( - window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' }) - )).then(() => { - navigate('/dashboard/users', { - state: { openSavedToast: true } - }); - loading.hide(); - }).catch(err => { - console.error('[useredit] failed to update user', err); + updateUser.mutate({ userId: user.Id, userDto: user }, { + onSuccess: () => { + if (user.Id) { + updateUserPolicy.mutate({ + userId: user.Id, + userPolicy: user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' } + }, { + onSuccess: () => { + navigate('/dashboard/users', { + state: { openSavedToast: true } + }); + } + }); + } + } }); }; const onSubmit = (e: Event) => { loading.show(); - getUser().then(function (result) { - saveUser(result); - }).catch(err => { - console.error('[useredit] failed to fetch user', err); - }); + if (userDto) { + saveUser(userDto); + } e.preventDefault(); e.stopPropagation(); return false; }; + const onBtnCancelClick = () => { + window.history.back(); + }; + (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) { (page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked); }); - window.ApiClient.getNamedConfiguration('network').then(function (config) { - (page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess); - }).catch(err => { - console.error('[useredit] failed to load network config', err); - }); - (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); + (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', onBtnCancelClick); - (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() { - window.history.back(); - }); - }, [loadData]); + return () => { + (page.querySelector('.editUserProfileForm') as HTMLFormElement).removeEventListener('submit', onSubmit); + (page.querySelector('#btnCancel') as HTMLButtonElement).removeEventListener('click', onBtnCancelClick); + }; + }, [loadData, updateUser, userDto, updateUserPolicy, navigate]); - const optionLoginProvider = authProviders.map((provider) => { + const optionLoginProvider = authProviders?.map((provider) => { const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : ''; return ``; }); - const optionPasswordResetProvider = passwordResetProviders.map((provider) => { + const optionPasswordResetProvider = passwordResetProviders?.map((provider) => { const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : ''; return ``; }); diff --git a/src/apps/stable/routes/user/userprofile.tsx b/src/apps/stable/routes/user/userprofile.tsx index bc08fe4b09..ad584270d4 100644 --- a/src/apps/stable/routes/user/userprofile.tsx +++ b/src/apps/stable/routes/user/userprofile.tsx @@ -1,6 +1,6 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client'; import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type'; -import React, { FunctionComponent, useEffect, useState, useRef, useCallback, useMemo } from 'react'; +import React, { FunctionComponent, useEffect, useRef, useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import Dashboard from '../../../../utils/dashboard'; @@ -9,15 +9,21 @@ import { appHost } from '../../../../components/apphost'; import confirm from '../../../../components/confirm/confirm'; import Button from '../../../../elements/emby-button/Button'; import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm'; -import loading from '../../../../components/loading/loading'; import toast from '../../../../components/toast/toast'; import Page from '../../../../components/Page'; +<<<<<<< HEAD import { AppFeature } from 'constants/appFeature'; +======= +import { useUser } from 'apps/dashboard/features/users/api/useUser'; +import Loading from 'components/loading/LoadingComponent'; +import loading from 'components/loading/loading'; +import { queryClient } from 'utils/query/queryClient'; +>>>>>>> 9445839e9 (Move user pages to TS SDK) const UserProfile: FunctionComponent = () => { const [ searchParams ] = useSearchParams(); const userId = searchParams.get('userId'); - const [ userName, setUserName ] = useState(''); + const { data: user, isPending: isUserPending } = useUser(userId ? { userId: userId } : undefined); const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); const element = useRef(null); @@ -30,17 +36,13 @@ const UserProfile: FunctionComponent = () => { return; } - if (!userId) { - console.error('[userprofile] missing user id'); - return; + if (!user?.Name || !user?.Id) { + throw new Error('Unexpected null user name or id'); } - loading.show(); - window.ApiClient.getUser(userId).then(function (user) { - if (!user.Name || !user.Id) { - throw new Error('Unexpected null user name or id'); - } + void libraryMenu.then(menu => menu.setTitle(user.Name)); +<<<<<<< HEAD setUserName(user.Name); void libraryMenu.then(menu => menu.setTitle(user.Name)); @@ -68,12 +70,34 @@ const UserProfile: FunctionComponent = () => { } }).catch(err => { console.error('[userprofile] failed to get current user', err); +======= + let imageUrl = 'assets/img/avatar.png'; + if (user.PrimaryImageTag) { + imageUrl = window.ApiClient.getUserImageUrl(user.Id, { + tag: user.PrimaryImageTag, + type: 'Primary' +>>>>>>> 9445839e9 (Move user pages to TS SDK) }); - loading.hide(); + } + const userImage = (page.querySelector('#image') as HTMLDivElement); + userImage.style.backgroundImage = 'url(' + imageUrl + ')'; + + Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) { + if (!user.Policy) { + throw new Error('Unexpected null user.Policy'); + } + + if (user.PrimaryImageTag) { + (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide'); + (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide'); + } else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { + (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide'); + (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide'); + } }).catch(err => { - console.error('[userprofile] failed to load data', err); + console.error('[userprofile] failed to get current user', err); }); - }, [userId]); + }, [user]); useEffect(() => { const page = element.current; @@ -125,7 +149,9 @@ const UserProfile: FunctionComponent = () => { userImage.style.backgroundImage = 'url(' + reader.result + ')'; window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () { loading.hide(); - reloadUser(); + void queryClient.invalidateQueries({ + queryKey: ['User'] + }); }).catch(err => { console.error('[userprofile] failed to upload image', err); }); @@ -134,7 +160,7 @@ const UserProfile: FunctionComponent = () => { reader.readAsDataURL(file); }; - (page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () { + const onDeleteImageClick = function () { if (!userId) { console.error('[userprofile] missing user id'); return; @@ -147,25 +173,41 @@ const UserProfile: FunctionComponent = () => { loading.show(); window.ApiClient.deleteUserImage(userId, ImageType.Primary).then(function () { loading.hide(); - reloadUser(); + void queryClient.invalidateQueries({ + queryKey: ['User'] + }); }).catch(err => { console.error('[userprofile] failed to delete image', err); }); }).catch(() => { // confirm dialog closed }); - }); + }; - (page.querySelector('#btnAddImage') as HTMLButtonElement).addEventListener('click', function () { + const addImageClick = function () { const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement; uploadImage.value = ''; uploadImage.click(); - }); + }; - (page.querySelector('#uploadImage') as HTMLInputElement).addEventListener('change', function (evt: Event) { - setFiles(evt); - }); - }, [reloadUser, userId]); + const onUploadImage = (e: Event) => { + setFiles(e); + }; + + (page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', onDeleteImageClick); + (page.querySelector('#btnAddImage') as HTMLButtonElement).addEventListener('click', addImageClick); + (page.querySelector('#uploadImage') as HTMLInputElement).addEventListener('change', onUploadImage); + + return () => { + (page.querySelector('#btnDeleteImage') as HTMLButtonElement).removeEventListener('click', onDeleteImageClick); + (page.querySelector('#btnAddImage') as HTMLButtonElement).removeEventListener('click', addImageClick); + (page.querySelector('#uploadImage') as HTMLInputElement).removeEventListener('change', onUploadImage); + }; + }, [reloadUser, user, userId]); + + if (isUserPending || !user) { + return ; + } return ( {

- {userName} + {user?.Name}


diff --git a/src/components/dashboard/users/UserPasswordForm.tsx b/src/components/dashboard/users/UserPasswordForm.tsx index b6e05e41df..b3a0634127 100644 --- a/src/components/dashboard/users/UserPasswordForm.tsx +++ b/src/components/dashboard/users/UserPasswordForm.tsx @@ -9,12 +9,11 @@ import Button from '../../../elements/emby-button/Button'; import Input from '../../../elements/emby-input/Input'; type IProps = { - userId: string | null; + user: UserDto }; -const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { +const UserPasswordForm: FunctionComponent = ({ user }: IProps) => { const element = useRef(null); - const user = useRef(); const libraryMenu = useMemo(async () => ((await import('../../../scripts/libraryMenu')).default), []); const loadUser = useCallback(async () => { @@ -25,22 +24,16 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { return; } - if (!userId) { - console.error('[UserPasswordForm] missing user id'); - return; - } - - user.current = await window.ApiClient.getUser(userId); const loggedInUser = await Dashboard.getCurrentUser(); - if (!user.current.Policy || !user.current.Configuration) { + if (!user.Policy || !user.Configuration) { throw new Error('Unexpected null user policy or configuration'); } - (await libraryMenu).setTitle(user.current.Name); + (await libraryMenu).setTitle(user.Name); - if (user.current.HasConfiguredPassword) { - if (!user.current.Policy?.IsAdministrator) { + if (user.HasConfiguredPassword) { + if (!user.Policy?.IsAdministrator) { (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide'); } (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide'); @@ -49,7 +42,7 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide'); } - const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.current.Policy.EnableUserPreferenceAccess; + const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess; (page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword); import('../../autoFocuser').then(({ default: autoFocuser }) => { @@ -61,7 +54,7 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { (page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = ''; (page.querySelector('#txtNewPassword') as HTMLInputElement).value = ''; (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value = ''; - }, [userId]); + }, [user]); useEffect(() => { const page = element.current; @@ -78,7 +71,7 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { const onSubmit = (e: Event) => { if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value != (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value) { toast(globalize.translate('PasswordMatchError')); - } else if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value == '' && user.current?.Policy?.IsAdministrator) { + } else if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value == '' && user?.Policy?.IsAdministrator) { toast(globalize.translate('PasswordMissingSaveError')); } else { loading.show(); @@ -90,7 +83,7 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { }; const savePassword = () => { - if (!userId) { + if (!user.Id) { console.error('[UserPasswordForm.savePassword] missing user id'); return; } @@ -104,7 +97,7 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { currentPassword = ''; } - window.ApiClient.updateUserPassword(userId, currentPassword, newPassword).then(function () { + window.ApiClient.updateUserPassword(user.Id, currentPassword, newPassword).then(function () { loading.hide(); toast(globalize.translate('PasswordSaved')); @@ -121,26 +114,23 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { }; const resetPassword = () => { - if (!userId) { - console.error('[UserPasswordForm.resetPassword] missing user id'); - return; - } - const msg = globalize.translate('PasswordResetConfirmation'); confirm(msg, globalize.translate('ResetPassword')).then(function () { loading.show(); - window.ApiClient.resetUserPassword(userId).then(function () { - loading.hide(); - Dashboard.alert({ - message: globalize.translate('PasswordResetComplete'), - title: globalize.translate('ResetPassword') + if (user.Id) { + window.ApiClient.resetUserPassword(user.Id).then(function () { + loading.hide(); + Dashboard.alert({ + message: globalize.translate('PasswordResetComplete'), + title: globalize.translate('ResetPassword') + }); + loadUser().catch(err => { + console.error('[UserPasswordForm] failed to load user', err); + }); + }).catch(err => { + console.error('[UserPasswordForm] failed to reset user password', err); }); - loadUser().catch(err => { - console.error('[UserPasswordForm] failed to load user', err); - }); - }).catch(err => { - console.error('[UserPasswordForm] failed to reset user password', err); - }); + } }).catch(() => { // confirm dialog was closed }); @@ -148,7 +138,12 @@ const UserPasswordForm: FunctionComponent = ({ userId }: IProps) => { (page.querySelector('.updatePasswordForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('#btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword); - }, [loadUser, userId]); + + return () => { + (page.querySelector('.updatePasswordForm') as HTMLFormElement).removeEventListener('submit', onSubmit); + (page.querySelector('#btnResetPassword') as HTMLButtonElement).removeEventListener('click', resetPassword); + }; + }, [loadUser, user]); return (
diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index ec11920f87..94f9eabd2a 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -8,6 +8,8 @@ import { useApi } from './useApi'; export type UsersRecords = Record; +export const QUERY_KEY = 'Users'; + const fetchUsers = async ( api: Api, requestParams?: UserApiGetUsersRequest, @@ -23,7 +25,7 @@ const fetchUsers = async ( export const useUsers = (requestParams?: UserApiGetUsersRequest) => { const { api } = useApi(); return useQuery({ - queryKey: ['Users'], + queryKey: [ QUERY_KEY ], queryFn: ({ signal }) => fetchUsers(api!, requestParams, { signal }), enabled: !!api