diff --git a/src/apps/dashboard/components/UserAvatarButton.tsx b/src/apps/dashboard/components/UserAvatarButton.tsx index fdf68ccf00..543fcd00f4 100644 --- a/src/apps/dashboard/components/UserAvatarButton.tsx +++ b/src/apps/dashboard/components/UserAvatarButton.tsx @@ -25,7 +25,7 @@ const UserAvatarButton: FC = ({ }} title={user.Name || undefined} component={Link} - to={`/dashboard/users/profile?userId=${user.Id}`} + to={`/dashboard/users/${user.Id}/profile`} > diff --git a/src/apps/dashboard/constants/helpLinks.ts b/src/apps/dashboard/constants/helpLinks.ts index bf7fa04dc0..a092dcbd72 100644 --- a/src/apps/dashboard/constants/helpLinks.ts +++ b/src/apps/dashboard/constants/helpLinks.ts @@ -40,12 +40,7 @@ export const HelpLinks = [ paths: ['/dashboard/users'], url: 'https://jellyfin.org/docs/general/server/users/adding-managing-users' }, { - paths: [ - '/dashboard/users/access', - '/dashboard/users/parentalcontrol', - '/dashboard/users/password', - '/dashboard/users/profile' - ], + paths: ['/dashboard/users/:userId/:tab'], url: 'https://jellyfin.org/docs/general/server/users/' }, { paths: ['/dashboard/backups'], diff --git a/src/apps/dashboard/routes/users/access.tsx b/src/apps/dashboard/features/users/components/Access.tsx similarity index 66% rename from src/apps/dashboard/routes/users/access.tsx rename to src/apps/dashboard/features/users/components/Access.tsx index 3fa9e56328..c122e98fec 100644 --- a/src/apps/dashboard/routes/users/access.tsx +++ b/src/apps/dashboard/features/users/components/Access.tsx @@ -1,17 +1,17 @@ import type { BaseItemDto, DeviceInfoDto, UserDto } from '@jellyfin/sdk/lib/generated-client'; import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import loading from '../../../../components/loading/loading'; -import globalize from '../../../../lib/globalize'; -import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; -import Button from '../../../../elements/emby-button/Button'; -import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; -import AccessContainer from '../../../../components/dashboard/users/AccessContainer'; -import CheckBoxElement from '../../../../elements/CheckBoxElement'; -import Page from '../../../../components/Page'; +import loading from 'components/loading/loading'; +import globalize from 'lib/globalize'; +import Button from 'elements/emby-button/Button'; +import AccessContainer from 'components/dashboard/users/AccessContainer'; +import CheckBoxElement from 'elements/CheckBoxElement'; import Toast from 'apps/dashboard/components/Toast'; +interface AccessProps { + userId: string; +} + type ItemsArr = { Name?: string | null; Id?: string | null; @@ -20,15 +20,12 @@ type ItemsArr = { checkedAttribute?: string }; -const UserLibraryAccess = () => { - const [ searchParams ] = useSearchParams(); - const userId = searchParams.get('userId'); +const Access = ({ userId }: AccessProps) => { const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false); - const [ userName, setUserName ] = useState(''); const [channelsItems, setChannelsItems] = useState([]); const [mediaFoldersItems, setMediaFoldersItems] = useState([]); const [devicesItems, setDevicesItems] = useState([]); - const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); + const libraryMenu = useMemo(async () => ((await import('scripts/libraryMenu')).default), []); const element = useRef(null); @@ -137,7 +134,6 @@ const UserLibraryAccess = () => { }, []); const loadUser = useCallback((user: UserDto, mediaFolders: BaseItemDto[], channels: BaseItemDto[], devices: DeviceInfoDto[]) => { - setUserName(user.Name || ''); void libraryMenu.then(menu => menu.setTitle(user.Name)); loadChannels(user, channels); loadMediaFolders(user, mediaFolders); @@ -244,99 +240,87 @@ const UserLibraryAccess = () => { }, [loadData]); return ( - +
-
-
- + + {mediaFoldersItems.map(Item => ( + + ))} + + + + {channelsItems.map(Item => ( + + ))} + + + + {devicesItems.map(Item => ( + + ))} + +
+
+
- -
- - {mediaFoldersItems.map(Item => ( - - ))} - - - - {channelsItems.map(Item => ( - - ))} - - - - {devicesItems.map(Item => ( - - ))} - -
-
-
-
-
- - + +
); }; -export default UserLibraryAccess; +export default Access; diff --git a/src/apps/dashboard/routes/users/parentalcontrol.tsx b/src/apps/dashboard/features/users/components/ParentalControl.tsx similarity index 67% rename from src/apps/dashboard/routes/users/parentalcontrol.tsx rename to src/apps/dashboard/features/users/components/ParentalControl.tsx index b1e5471209..9fe721df43 100644 --- a/src/apps/dashboard/routes/users/parentalcontrol.tsx +++ b/src/apps/dashboard/features/users/components/ParentalControl.tsx @@ -3,22 +3,23 @@ import { UnratedItem } from '@jellyfin/sdk/lib/generated-client/models/unrated-i import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week'; import escapeHTML from 'escape-html'; import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import globalize from '../../../../lib/globalize'; -import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList'; -import TagList from '../../../../components/dashboard/users/TagList'; -import Button from '../../../../elements/emby-button/Button'; -import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; -import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; -import loading from '../../../../components/loading/loading'; -import CheckBoxElement from '../../../../elements/CheckBoxElement'; -import SelectElement from '../../../../elements/SelectElement'; -import Page from '../../../../components/Page'; -import prompt from '../../../../components/prompt/prompt'; +import globalize from 'lib/globalize'; +import AccessScheduleList from 'components/dashboard/users/AccessScheduleList'; +import TagList from 'components/dashboard/users/TagList'; +import Button from 'elements/emby-button/Button'; +import SectionTitleContainer from 'elements/SectionTitleContainer'; +import loading from 'components/loading/loading'; +import CheckBoxElement from 'elements/CheckBoxElement'; +import SelectElement from 'elements/SelectElement'; +import prompt from 'components/prompt/prompt'; import { ServerConnections } from 'lib/jellyfin-apiclient'; import Toast from 'apps/dashboard/components/Toast'; +interface ParentalControlProps { + userId: string; +} + type NamedItem = { name: string; value: UnratedItem; @@ -64,10 +65,7 @@ function handleSaveUser( }; } -const UserParentalControl = () => { - const [ searchParams ] = useSearchParams(); - const userId = searchParams.get('userId'); - const [ userName, setUserName ] = useState(''); +const ParentalControl = ({ userId }: ParentalControlProps) => { const [ parentalRatings, setParentalRatings ] = useState([]); const [ unratedItems, setUnratedItems ] = useState([]); const [ maxParentalRating, setMaxParentalRating ] = useState(); @@ -75,7 +73,7 @@ const UserParentalControl = () => { const [ allowedTags, setAllowedTags ] = useState([]); const [ blockedTags, setBlockedTags ] = useState([]); const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false); - const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); + const libraryMenu = useMemo(async () => ((await import('scripts/libraryMenu')).default), []); const element = useRef(null); const parentalRatingsRef = useRef([]); @@ -141,7 +139,6 @@ const UserParentalControl = () => { return; } - setUserName(user.Name || ''); void libraryMenu.then(menu => menu.setTitle(user.Name)); loadUnratedItems(user); @@ -231,7 +228,7 @@ const UserParentalControl = () => { const showSchedulePopup = (schedule: AccessSchedule, index: number) => { schedule = schedule || {}; - import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { + import('components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => { accessschedule.show({ schedule: schedule }).then(function (updatedSchedule) { @@ -388,136 +385,124 @@ const UserParentalControl = () => { }, [accessSchedules, setAccessSchedules]); return ( - +
-
-
+
+
+ + {optionMaxParentalRating()} + +
+ {globalize.translate('MaxParentalRatingHelp')} +
+
+
+
+

+ {globalize.translate('HeaderBlockItemsWithNoRating')} +

+
+ {unratedItems.map(Item => { + return ; + })} +
+
+
+
+
+
+ {globalize.translate('AllowContentWithTagsHelp')} +
+
+ {allowedTags?.map(tag => { + return ; + })} +
+
+
+ +
+ {globalize.translate('BlockContentWithTagsHelp')} +
+
+ {blockedTags.map(tag => { + return ; + })} +
+
+
+ +

{globalize.translate('HeaderAccessScheduleHelp')}

+
+ {accessSchedules.map((accessSchedule, index) => { + return ; + })} +
+
+
+
- - -
- - {optionMaxParentalRating()} - -
- {globalize.translate('MaxParentalRatingHelp')} -
-
-
-
-

- {globalize.translate('HeaderBlockItemsWithNoRating')} -

-
- {unratedItems.map(Item => { - return ; - })} -
-
-
-
-
- -
- {globalize.translate('AllowContentWithTagsHelp')} -
-
- {allowedTags?.map(tag => { - return ; - })} -
-
-
- -
- {globalize.translate('BlockContentWithTagsHelp')} -
-
- {blockedTags.map(tag => { - return ; - })} -
-
-
- -

{globalize.translate('HeaderAccessScheduleHelp')}

-
- {accessSchedules.map((accessSchedule, index) => { - return ; - })} -
-
-
-
- -
- - + +
); }; -export default UserParentalControl; +export default ParentalControl; diff --git a/src/apps/dashboard/features/users/components/Password.tsx b/src/apps/dashboard/features/users/components/Password.tsx new file mode 100644 index 0000000000..4e61121a4c --- /dev/null +++ b/src/apps/dashboard/features/users/components/Password.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import UserPasswordForm from 'components/dashboard/users/UserPasswordForm'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; + +interface PasswordProps { + user: UserDto; +} + +const Password = ({ user }: PasswordProps) => { + return ( +
+
+ +
+
+ ); +}; + +export default Password; diff --git a/src/apps/dashboard/routes/users/profile.tsx b/src/apps/dashboard/features/users/components/Profile.tsx similarity index 50% rename from src/apps/dashboard/routes/users/profile.tsx rename to src/apps/dashboard/features/users/components/Profile.tsx index 3d84df42fa..ec3268aceb 100644 --- a/src/apps/dashboard/routes/users/profile.tsx +++ b/src/apps/dashboard/features/users/components/Profile.tsx @@ -1,19 +1,15 @@ import type { BaseItemDto, NameIdPair, SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client'; import escapeHTML from 'escape-html'; import React, { useCallback, useEffect, useState, useRef, useMemo } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; -import globalize from '../../../../lib/globalize'; -import Button from '../../../../elements/emby-button/Button'; -import CheckBoxElement from '../../../../elements/CheckBoxElement'; -import LinkButton from '../../../../elements/emby-button/LinkButton'; -import Input from '../../../../elements/emby-input/Input'; -import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; -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 globalize from 'lib/globalize'; +import Button from 'elements/emby-button/Button'; +import CheckBoxElement from 'elements/CheckBoxElement'; +import LinkButton from 'elements/emby-button/LinkButton'; +import Input from 'elements/emby-input/Input'; +import loading from 'components/loading/loading'; +import SelectElement from 'elements/SelectElement'; 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'; @@ -22,6 +18,10 @@ 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'; +interface ProfileProps { + userDto: UserDto; +} + type ResetProvider = BaseItemDto & { checkedAttribute: string }; @@ -31,17 +31,14 @@ const getCheckedElementDataIds = (elements: NodeListOf) => ( .map(e => e.getAttribute('data-id')) ); -const UserEdit = () => { +const Profile = ({ userDto }: ProfileProps) => { const navigate = useNavigate(); - const [ searchParams ] = useSearchParams(); - const userId = searchParams.get('userId'); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]); - const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []); + 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 }); @@ -158,7 +155,7 @@ const UserEdit = () => { } }, [netConfig, isNetConfigSuccess]); - const loadUser = useCallback((user: UserDto) => { + const loadUser = useCallback(() => { const page = element.current; if (!page) { @@ -167,53 +164,42 @@ const UserEdit = () => { } const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement; - disabledUserBanner.classList.toggle('hide', !user.Policy?.IsDisabled); + disabledUserBanner.classList.toggle('hide', !userDto.Policy?.IsDisabled); const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement; txtUserName.disabled = false; txtUserName.removeAttribute('disabled'); - void libraryMenu.then(menu => menu.setTitle(user.Name)); + void libraryMenu.then(menu => menu.setTitle(userDto.Name)); - (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; - (page.querySelector('.chkIsHidden') as HTMLInputElement).checked = !!user.Policy?.IsHidden; - (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = !!user.Policy?.EnableCollectionManagement; - (page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = !!user.Policy?.EnableSubtitleManagement; - (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = !!user.Policy?.EnableSharedDeviceControl; - (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = !!user.Policy?.EnableRemoteControlOfOtherUsers; - (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = !!user.Policy?.EnableContentDownloading; - (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvManagement; - (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = !!user.Policy?.EnableLiveTvAccess; - (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = !!user.Policy?.EnableMediaPlayback; - (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableAudioPlaybackTranscoding; - (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = !!user.Policy?.EnableVideoPlaybackTranscoding; - (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = !!user.Policy?.EnablePlaybackRemuxing; - (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = !!user.Policy?.ForceRemoteSourceTranscoding; - (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy?.EnableRemoteAccess == null || user.Policy?.EnableRemoteAccess; - (page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy?.RemoteClientBitrateLimit && user.Policy?.RemoteClientBitrateLimit > 0 ? - (user.Policy?.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : ''; - (page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = String(user.Policy?.LoginAttemptsBeforeLockout) || '-1'; - (page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(user.Policy?.MaxActiveSessions) || '0'; - (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(user.Policy?.SyncPlayAccess); + (page.querySelector('#txtUserName') as HTMLInputElement).value = userDto.Name || ''; + (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!userDto.Policy?.IsAdministrator; + (page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!userDto.Policy?.IsDisabled; + (page.querySelector('.chkIsHidden') as HTMLInputElement).checked = !!userDto.Policy?.IsHidden; + (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = !!userDto.Policy?.EnableCollectionManagement; + (page.querySelector('.chkEnableSubtitleManagement') as HTMLInputElement).checked = !!userDto.Policy?.EnableSubtitleManagement; + (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = !!userDto.Policy?.EnableSharedDeviceControl; + (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = !!userDto.Policy?.EnableRemoteControlOfOtherUsers; + (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = !!userDto.Policy?.EnableContentDownloading; + (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = !!userDto.Policy?.EnableLiveTvManagement; + (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = !!userDto.Policy?.EnableLiveTvAccess; + (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = !!userDto.Policy?.EnableMediaPlayback; + (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = !!userDto.Policy?.EnableAudioPlaybackTranscoding; + (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = !!userDto.Policy?.EnableVideoPlaybackTranscoding; + (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = !!userDto.Policy?.EnablePlaybackRemuxing; + (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = !!userDto.Policy?.ForceRemoteSourceTranscoding; + (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = userDto.Policy?.EnableRemoteAccess == null || userDto.Policy?.EnableRemoteAccess; + (page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = userDto.Policy?.RemoteClientBitrateLimit && userDto.Policy?.RemoteClientBitrateLimit > 0 ? + (userDto.Policy?.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : ''; + (page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = String(userDto.Policy?.LoginAttemptsBeforeLockout) || '-1'; + (page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = String(userDto.Policy?.MaxActiveSessions) || '0'; + (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = String(userDto.Policy?.SyncPlayAccess); loading.hide(); - }, [ libraryMenu ]); - - const loadData = useCallback(() => { - if (!userDto) { - console.error('[profile] No user available'); - return; - } - loading.show(); - loadUser(userDto); - }, [userDto, loadUser]); + }, [ userDto, libraryMenu ]); useEffect(() => { - if (isUserSuccess) { - loadData(); - } - }, [loadData, isUserSuccess]); + loadUser(); + }, [ loadUser ]); useEffect(() => { const page = element.current; @@ -298,7 +284,7 @@ const UserEdit = () => { (page.querySelector('.editUserProfileForm') as HTMLFormElement).removeEventListener('submit', onSubmit); (page.querySelector('#btnCancel') as HTMLButtonElement).removeEventListener('click', onBtnCancelClick); }; - }, [loadData, updateUser, userDto, updateUserPolicy, navigate]); + }, [updateUser, userDto, updateUserPolicy, navigate]); const optionLoginProvider = authProviders?.map((provider) => { const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : ''; @@ -319,294 +305,281 @@ const UserEdit = () => { }; return ( - -
-
- -
- - -
- - {globalize.translate('ButtonEditOtherUserPreferences')} - -
-
-
-
-
- {globalize.translate('HeaderThisUserIsCurrentlyDisabled')} -
-
- {globalize.translate('MessageReenableUser')} -
-
-
-
- -
-
- - {optionLoginProvider} - - -
- {globalize.translate('AuthProviderHelp')} -
-
-
- - {optionPasswordResetProvider} - -
- {globalize.translate('PasswordResetProviderHelp')} -
-
-
- -
- {globalize.translate('AllowRemoteAccessHelp')} -
-
- - - -
-

- {globalize.translate('HeaderFeatureAccess')} -

-
- - -
-
-
-

- {globalize.translate('HeaderPlayback')} -

-
- - - - - -
-
- {globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')} -
-
-
-
-
- -
- {globalize.translate('LabelRemoteClientBitrateLimitHelp')} -
-
- {globalize.translate('LabelUserRemoteClientBitrateLimitHelp')} -
-
-
-
-
- - {optionSyncPlayAccess()} - -
- {globalize.translate('SyncPlayAccessHelp')} -
-
-
-
-

- {globalize.translate('HeaderAllowMediaDeletionFrom')} -

-
- -
- {deleteFoldersAccess.map(Item => ( - - ))} -
-
-
-
-

- {globalize.translate('HeaderRemoteControl')} -

-
- - -
-
- {globalize.translate('OptionAllowRemoteSharedDevicesHelp')} -
-
-

- {globalize.translate('Other')} -

-
- -
- {globalize.translate('OptionAllowContentDownloadHelp')} -
-
-
- -
- {globalize.translate('OptionDisableUserHelp')} -
-
-
- -
- {globalize.translate('OptionHideUserFromLoginHelp')} -
-
-
-
-
- -
- {globalize.translate('OptionLoginAttemptsBeforeLockout')} -
-
- {globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')} -
-
-
-
-
-
- -
- {globalize.translate('OptionMaxActiveSessions')} -
-
- {globalize.translate('OptionMaxActiveSessionsHelp')} -
-
-
-
-
-
- +
+
+ + {globalize.translate('ButtonEditOtherUserPreferences')} +
- +
+
+
+
+ {globalize.translate('HeaderThisUserIsCurrentlyDisabled')} +
+
+ {globalize.translate('MessageReenableUser')} +
+
+
+
+ +
+
+ + {optionLoginProvider} + +
+ {globalize.translate('AuthProviderHelp')} +
+
+
+ + {optionPasswordResetProvider} + +
+ {globalize.translate('PasswordResetProviderHelp')} +
+
+
+ +
+ {globalize.translate('AllowRemoteAccessHelp')} +
+
+ + + +
+

+ {globalize.translate('HeaderFeatureAccess')} +

+
+ + +
+
+
+

+ {globalize.translate('HeaderPlayback')} +

+
+ + + + + +
+
+ {globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')} +
+
+
+
+
+ +
+ {globalize.translate('LabelRemoteClientBitrateLimitHelp')} +
+
+ {globalize.translate('LabelUserRemoteClientBitrateLimitHelp')} +
+
+
+
+
+ + {optionSyncPlayAccess()} + +
+ {globalize.translate('SyncPlayAccessHelp')} +
+
+
+
+

+ {globalize.translate('HeaderAllowMediaDeletionFrom')} +

+
+ +
+ {deleteFoldersAccess.map(Item => ( + + ))} +
+
+
+
+

+ {globalize.translate('HeaderRemoteControl')} +

+
+ + +
+
+ {globalize.translate('OptionAllowRemoteSharedDevicesHelp')} +
+
+

+ {globalize.translate('Other')} +

+
+ +
+ {globalize.translate('OptionAllowContentDownloadHelp')} +
+
+
+ +
+ {globalize.translate('OptionDisableUserHelp')} +
+
+
+ +
+ {globalize.translate('OptionHideUserFromLoginHelp')} +
+
+
+
+
+ +
+ {globalize.translate('OptionLoginAttemptsBeforeLockout')} +
+
+ {globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')} +
+
+
+
+
+
+ +
+ {globalize.translate('OptionMaxActiveSessions')} +
+
+ {globalize.translate('OptionMaxActiveSessionsHelp')} +
+
+
+
+
+
+ +
); }; -export default UserEdit; +export default Profile; diff --git a/src/apps/dashboard/features/users/constants/userTab.ts b/src/apps/dashboard/features/users/constants/userTab.ts new file mode 100644 index 0000000000..3519c324a9 --- /dev/null +++ b/src/apps/dashboard/features/users/constants/userTab.ts @@ -0,0 +1,6 @@ +export enum UserTab { + Profile = 'profile', + Access = 'access', + ParentalControl = 'parentalcontrol', + Password = 'password' +} diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index fa3512ac64..e8411bdc27 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -28,9 +28,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'tasks', type: AppType.Dashboard }, { path: 'tasks/:id', page: 'tasks/task', type: AppType.Dashboard }, { path: 'users', type: AppType.Dashboard }, - { path: 'users/access', type: AppType.Dashboard }, { path: 'users/add', type: AppType.Dashboard }, - { path: 'users/parentalcontrol', type: AppType.Dashboard }, - { path: 'users/password', type: AppType.Dashboard }, - { path: 'users/profile', type: AppType.Dashboard } + { path: 'users/:userId/:tab', page: 'users/edit', type: AppType.Dashboard } ]; diff --git a/src/apps/dashboard/routes/users/add.tsx b/src/apps/dashboard/routes/users/add.tsx index e38c39f208..a00dfb338b 100644 --- a/src/apps/dashboard/routes/users/add.tsx +++ b/src/apps/dashboard/routes/users/add.tsx @@ -160,7 +160,7 @@ const UserNew = () => { userPolicy: user.Policy }, { onSuccess: () => { - navigate(`/dashboard/users/profile?userId=${user.Id}`); + navigate(`/dashboard/users/${user.Id}/profile`); }, onError: () => { console.error('[usernew] failed to update user policy'); diff --git a/src/apps/dashboard/routes/users/edit.tsx b/src/apps/dashboard/routes/users/edit.tsx new file mode 100644 index 0000000000..7e2e31787a --- /dev/null +++ b/src/apps/dashboard/routes/users/edit.tsx @@ -0,0 +1,72 @@ +import React, { useCallback } from 'react'; +import { useUser } from 'apps/dashboard/features/users/api/useUser'; +import Loading from 'components/loading/LoadingComponent'; +import Page from 'components/Page'; +import { useNavigate, useParams } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import globalize from 'lib/globalize'; +import Stack from '@mui/material/Stack'; +import { UserTab } from 'apps/dashboard/features/users/constants/userTab'; +import Profile from 'apps/dashboard/features/users/components/Profile'; +import Access from 'apps/dashboard/features/users/components/Access'; +import ParentalControl from 'apps/dashboard/features/users/components/ParentalControl'; +import Password from 'apps/dashboard/features/users/components/Password'; +import Alert from '@mui/material/Alert'; + +export const Component = () => { + const navigate = useNavigate(); + const { userId, tab } = useParams(); + const { data: user, isPending, isError } = useUser(userId ? { userId: userId } : undefined); + + const handleTabChange = useCallback((event: React.SyntheticEvent, newValue: UserTab) => { + navigate(`/dashboard/users/${userId}/${newValue}`); + }, [ navigate ]); + + if (isPending) return ; + + return ( + + + {isError ? ( + + {globalize.translate('UsersEditPageError')} + + ) : ( + + {user.Name} + + + + + + + + {tab == UserTab.Profile && ( + + )} + {tab == UserTab.Access && ( + + )} + {tab == UserTab.ParentalControl && ( + + )} + {tab == UserTab.Password && ( + + )} + + )} + + + ); +}; + +Component.displayName = 'UsersEditPage'; diff --git a/src/apps/dashboard/routes/users/index.tsx b/src/apps/dashboard/routes/users/index.tsx index add45e9270..1ef1fadced 100644 --- a/src/apps/dashboard/routes/users/index.tsx +++ b/src/apps/dashboard/routes/users/index.tsx @@ -15,6 +15,7 @@ import { useUsers } from 'hooks/useUsers'; import Loading from 'components/loading/LoadingComponent'; import { useDeleteUser } from 'apps/dashboard/features/users/api/useDeleteUser'; import dom from 'utils/dom'; +import { UserTab } from 'apps/dashboard/features/users/constants/userTab'; type MenuEntry = { name?: string; @@ -87,15 +88,15 @@ const UserProfiles = () => { callback: function (id: string) { switch (id) { case 'open': - navigate(`/dashboard/users/profile?userId=${userId}`); + navigate(`/dashboard/users/${userId}/${UserTab.Profile}`); break; case 'access': - navigate(`/dashboard/users/access?userId=${userId}`); + navigate(`/dashboard/users/${userId}/${UserTab.Access}`); break; case 'parentalcontrol': - navigate(`/dashboard/users/parentalcontrol?userId=${userId}`); + navigate(`/dashboard/users/${userId}/${UserTab.ParentalControl}`); break; case 'delete': diff --git a/src/apps/dashboard/routes/users/password.tsx b/src/apps/dashboard/routes/users/password.tsx deleted file mode 100644 index 36bfa9db5c..0000000000 --- a/src/apps/dashboard/routes/users/password.tsx +++ /dev/null @@ -1,43 +0,0 @@ -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 { 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 { data: user, isPending } = useUser(userId ? { userId: userId } : undefined); - - if (isPending || !user) { - return ; - } - - return ( - -
-
- -
- -
- -
-
-
- - ); -}; - -export default UserPassword; diff --git a/src/components/dashboard/users/SectionTabs.tsx b/src/components/dashboard/users/SectionTabs.tsx deleted file mode 100644 index 905e477b98..0000000000 --- a/src/components/dashboard/users/SectionTabs.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { FunctionComponent } from 'react'; - -import globalize from 'lib/globalize'; -import { navigate } from '../../../utils/dashboard'; -import LinkButton from '../../../elements/emby-button/LinkButton'; - -type IProps = { - activeTab: string; -}; - -function useNavigate(url: string): () => void { - return React.useCallback(() => { - navigate(url, true).catch(err => { - console.warn('Error navigating to dashboard url', err); - }); - }, [url]); -} - -const SectionTabs: FunctionComponent = ({ activeTab }: IProps) => { - const onClickProfile = useNavigate('/dashboard/users/profile'); - const onClickAccess = useNavigate('/dashboard/users/access'); - const onClickParentalControl = useNavigate('/dashboard/users/parentalcontrol'); - const clickPassword = useNavigate('/dashboard/users/password'); - return ( -
- - {globalize.translate('Profile')} - - - {globalize.translate('TabAccess')} - - - {globalize.translate('TabParentalControl')} - - - {globalize.translate('HeaderPassword')} - -
- ); -}; - -export default SectionTabs; diff --git a/src/components/dashboard/users/UserCardBox.tsx b/src/components/dashboard/users/UserCardBox.tsx index 22814d4435..35287ac9c3 100644 --- a/src/components/dashboard/users/UserCardBox.tsx +++ b/src/components/dashboard/users/UserCardBox.tsx @@ -57,7 +57,7 @@ const UserCardBox: FunctionComponent = ({ user = {} }: IProps) => {
+ href={`#/dashboard/users/${user.Id}/profile`}> {renderImgUrl}
diff --git a/src/strings/en-us.json b/src/strings/en-us.json index bd83b3feb3..7a44d3a867 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1694,6 +1694,7 @@ "UseEpisodeImagesInNextUpHelp": "'Next Up' and 'Continue Watching' sections will use episode images as thumbnails instead of the primary thumbnail of the show.", "UserAgentHelp": "Supply a custom 'User-Agent' HTTP header.", "UserMenu": "User Menu", + "UsersEditPageError": "Failed to load users edit page", "UserProfilesIntro": "Jellyfin includes support for user profiles with granular display settings, play state, and parental controls.", "ValueAlbumCount": "{0} albums", "ValueDiscNumber": "Disc {0}",