Merge pull request #7497 from viown/move-users-to-tabs

Move users edit page to tabs
This commit is contained in:
Bill Thornton
2026-03-19 19:34:59 -04:00
committed by GitHub
15 changed files with 636 additions and 705 deletions
@@ -25,7 +25,7 @@ const UserAvatarButton: FC<UserAvatarButtonProps> = ({
}}
title={user.Name || undefined}
component={Link}
to={`/dashboard/users/profile?userId=${user.Id}`}
to={`/dashboard/users/${user.Id}/profile`}
>
<UserAvatar user={user} />
</IconButton>
+1 -6
View File
@@ -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'],
@@ -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<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
const [devicesItems, setDevicesItems] = useState<ItemsArr[]>([]);
const libraryMenu = useMemo(async () => ((await import('../../../../scripts/libraryMenu')).default), []);
const libraryMenu = useMemo(async () => ((await import('scripts/libraryMenu')).default), []);
const element = useRef<HTMLDivElement>(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 (
<Page
id='userLibraryAccessPage'
className='mainAnimatedPage type-interior'
>
<div ref={element}>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
<form className='userLibraryAccessForm'>
<AccessContainer
containerClassName='folderAccessContainer'
headerTitle='HeaderLibraryAccess'
checkBoxClassName='chkEnableAllFolders'
checkBoxTitle='OptionEnableAccessToAllLibraries'
listContainerClassName='folderAccessListContainer'
accessClassName='folderAccess'
listTitle='HeaderLibraries'
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='channelAccessContainer hide'
headerTitle='HeaderChannelAccess'
checkBoxClassName='chkEnableAllChannels'
checkBoxTitle='OptionEnableAccessToAllChannels'
listContainerClassName='channelAccessListContainer'
accessClassName='channelAccess'
listTitle='Channels'
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkChannel'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='deviceAccessContainer hide'
headerTitle='HeaderDeviceAccess'
checkBoxClassName='chkEnableAllDevices'
checkBoxTitle='OptionEnableAccessFromAllDevices'
listContainerClassName='deviceAccessListContainer'
accessClassName='deviceAccess'
listTitle='HeaderDevices'
description='DeviceAccessHelp'
>
{devicesItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkDevice'
itemId={Item.Id}
itemName={Item.CustomName || Item.Name}
itemAppName={Item.AppName}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<br />
<div>
<Button
type='submit'
className='raised button-submit block'
title={globalize.translate('Save')}
/>
</div>
<SectionTabs activeTab='userlibraryaccess'/>
<form className='userLibraryAccessForm'>
<AccessContainer
containerClassName='folderAccessContainer'
headerTitle='HeaderLibraryAccess'
checkBoxClassName='chkEnableAllFolders'
checkBoxTitle='OptionEnableAccessToAllLibraries'
listContainerClassName='folderAccessListContainer'
accessClassName='folderAccess'
listTitle='HeaderLibraries'
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='channelAccessContainer hide'
headerTitle='HeaderChannelAccess'
checkBoxClassName='chkEnableAllChannels'
checkBoxTitle='OptionEnableAccessToAllChannels'
listContainerClassName='channelAccessListContainer'
accessClassName='channelAccess'
listTitle='Channels'
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkChannel'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='deviceAccessContainer hide'
headerTitle='HeaderDeviceAccess'
checkBoxClassName='chkEnableAllDevices'
checkBoxTitle='OptionEnableAccessFromAllDevices'
listContainerClassName='deviceAccessListContainer'
accessClassName='deviceAccess'
listTitle='HeaderDevices'
description='DeviceAccessHelp'
>
{devicesItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkDevice'
itemId={Item.Id}
itemName={Item.CustomName || Item.Name}
itemAppName={Item.AppName}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<br />
<div>
<Button
type='submit'
className='raised button-submit block'
title={globalize.translate('Save')}
/>
</div>
</form>
</div>
</Page>
</form>
</div>
);
};
export default UserLibraryAccess;
export default Access;
@@ -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<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedNamedItem[]>([]);
const [ maxParentalRating, setMaxParentalRating ] = useState<string>();
@@ -75,7 +73,7 @@ const UserParentalControl = () => {
const [ allowedTags, setAllowedTags ] = useState<string[]>([]);
const [ blockedTags, setBlockedTags ] = useState<string[]>([]);
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<HTMLDivElement>(null);
const parentalRatingsRef = useRef<ParentalRating[]>([]);
@@ -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 (
<Page
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<div ref={element}>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectElement
id='selectMaxParentalRating'
label='LabelMaxParentalRating'
>
{optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
</div>
<div>
<div className='blockUnratedItems'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderBlockItemsWithNoRating')}
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxElement
key={Item.value}
className='chkUnratedItem'
itemType={Item.value}
itemName={Item.name}
itemCheckedAttribute={Item.checkedAttribute}
/>;
})}
</div>
</div>
</div>
<br />
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
title={userName}
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelAllowContentWithTags')}
isBtnVisible={true}
btnId='btnAddAllowedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
/>
<div className='fieldDescription'>
{globalize.translate('AllowContentWithTagsHelp')}
</div>
<div className='allowedTags' style={{ marginTop: '.5em' }}>
{allowedTags?.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='allowedTag'
removeTagCallback={removeAllowedTagsCallback}
/>;
})}
</div>
</div>
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelBlockContentWithTags')}
isBtnVisible={true}
btnId='btnAddBlockedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
/>
<div className='fieldDescription'>
{globalize.translate('BlockContentWithTagsHelp')}
</div>
<div className='blockedTags' style={{ marginTop: '.5em' }}>
{blockedTags.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='blockedTag'
removeTagCallback={removeBlockedTagsTagsCallback}
/>;
})}
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
title={globalize.translate('HeaderAccessSchedule')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
/>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
return <AccessScheduleList
key={`${accessSchedule.DayOfWeek}${accessSchedule.StartHour}${accessSchedule.EndHour}`}
index={index}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
removeScheduleCallback={removeScheduleCallback}
/>;
})}
</div>
</div>
<div>
<Button
type='submit'
className='raised button-submit block'
title={globalize.translate('Save')}
/>
</div>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectElement
id='selectMaxParentalRating'
label='LabelMaxParentalRating'
>
{optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
</div>
<div>
<div className='blockUnratedItems'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderBlockItemsWithNoRating')}
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxElement
key={Item.value}
className='chkUnratedItem'
itemType={Item.value}
itemName={Item.name}
itemCheckedAttribute={Item.checkedAttribute}
/>;
})}
</div>
</div>
</div>
<br />
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelAllowContentWithTags')}
isBtnVisible={true}
btnId='btnAddAllowedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
/>
<div className='fieldDescription'>
{globalize.translate('AllowContentWithTagsHelp')}
</div>
<div className='allowedTags' style={{ marginTop: '.5em' }}>
{allowedTags?.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='allowedTag'
removeTagCallback={removeAllowedTagsCallback}
/>;
})}
</div>
</div>
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelBlockContentWithTags')}
isBtnVisible={true}
btnId='btnAddBlockedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
/>
<div className='fieldDescription'>
{globalize.translate('BlockContentWithTagsHelp')}
</div>
<div className='blockedTags' style={{ marginTop: '.5em' }}>
{blockedTags.map(tag => {
return <TagList
key={tag}
tag={tag}
tagType='blockedTag'
removeTagCallback={removeBlockedTagsTagsCallback}
/>;
})}
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
title={globalize.translate('HeaderAccessSchedule')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
/>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
return <AccessScheduleList
key={`${accessSchedule.DayOfWeek}${accessSchedule.StartHour}${accessSchedule.EndHour}`}
index={index}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
removeScheduleCallback={removeScheduleCallback}
/>;
})}
</div>
</div>
<div>
<Button
type='submit'
className='raised button-submit block'
title={globalize.translate('Save')}
/>
</div>
</form>
</div>
</Page>
</form>
</div>
);
};
export default UserParentalControl;
export default ParentalControl;
@@ -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 (
<div>
<div className='readOnlyContent'>
<UserPasswordForm
user={user}
/>
</div>
</div>
);
};
export default Password;
@@ -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<Element>) => (
.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<ResetProvider[]>([]);
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 (
<Page
id='editUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userDto?.Name || ''}
/>
</div>
<SectionTabs activeTab='useredit'/>
<div
className='lnkEditUserPreferencesContainer'
style={{ paddingBottom: '1em' }}
>
<LinkButton className='lnkEditUserPreferences button-link' href={userDto?.Id ? `mypreferencesmenu?userId=${userDto.Id}` : undefined}>
{globalize.translate('ButtonEditOtherUserPreferences')}
</LinkButton>
</div>
<form className='editUserProfileForm'>
<div className='disabledUserBanner hide'>
<div className='btn btnDarkAccent btnStatic'>
<div>
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
</div>
<div style={{ marginTop: 5 }}>
{globalize.translate('MessageReenableUser')}
</div>
</div>
</div>
<div id='fldUserName' className='inputContainer'>
<Input
type='text'
id='txtUserName'
label={globalize.translate('LabelName')}
required
/>
</div>
<div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement
id='selectLoginProvider'
label='LabelAuthProvider'
>
{optionLoginProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('AuthProviderHelp')}
</div>
</div>
<div className='selectContainer fldSelectPasswordResetProvider hide'>
<SelectElement
id='selectPasswordResetProvider'
label='LabelPasswordResetProvider'
>
{optionPasswordResetProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('PasswordResetProviderHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
<CheckBoxElement
className='chkRemoteAccess'
title='AllowRemoteAccess'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('AllowRemoteAccessHelp')}
</div>
</div>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkIsAdmin'
title='OptionAllowUserToManageServer'
/>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableCollectionManagement'
title='AllowCollectionManagement'
/>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableSubtitleManagement'
title='AllowSubtitleManagement'
/>
<div id='featureAccessFields' className='verticalSection'>
<h2 className='paperListLabel'>
{globalize.translate('HeaderFeatureAccess')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableLiveTvAccess'
title='OptionAllowBrowsingLiveTv'
/>
<CheckBoxElement
className='chkManageLiveTv'
title='OptionAllowManageLiveTv'
/>
</div>
</div>
<div className='verticalSection'>
<h2 className='paperListLabel'>
{globalize.translate('HeaderPlayback')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableMediaPlayback'
title='OptionAllowMediaPlayback'
/>
<CheckBoxElement
className='chkEnableAudioPlaybackTranscoding'
title='OptionAllowAudioPlaybackTranscoding'
/>
<CheckBoxElement
className='chkEnableVideoPlaybackTranscoding'
title='OptionAllowVideoPlaybackTranscoding'
/>
<CheckBoxElement
className='chkEnableVideoPlaybackRemuxing'
title='OptionAllowVideoPlaybackRemuxing'
/>
<CheckBoxElement
className='chkForceRemoteSourceTranscoding'
title='OptionForceRemoteSourceTranscoding'
/>
</div>
<div className='fieldDescription'>
{globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')}
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer'>
<Input
type='number'
id='txtRemoteClientBitrateLimit'
label={globalize.translate('LabelRemoteClientBitrateLimit')}
inputMode='decimal'
pattern='[0-9]*(.[0-9]+)?'
min='0'
step='.25'
/>
<div className='fieldDescription'>
{globalize.translate('LabelRemoteClientBitrateLimitHelp')}
</div>
<div className='fieldDescription'>
{globalize.translate('LabelUserRemoteClientBitrateLimitHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectSyncPlayAccess'>
<SelectElement
id='selectSyncPlayAccess'
label='LabelSyncPlayAccess'
>
{optionSyncPlayAccess()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('SyncPlayAccessHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<h2 className='checkboxListLabel' style={{ marginBottom: '1em' }}>
{globalize.translate('HeaderAllowMediaDeletionFrom')}
</h2>
<div className='checkboxList paperList checkboxList-paperList'>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableDeleteAllFolders'
title='AllLibraries'
/>
<div className='deleteAccess'>
{deleteFoldersAccess.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</div>
</div>
</div>
<div className='verticalSection'>
<h2 className='checkboxListLabel'>
{globalize.translate('HeaderRemoteControl')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableRemoteControlOtherUsers'
title='OptionAllowRemoteControlOthers'
/>
<CheckBoxElement
className='chkRemoteControlSharedDevices'
title='OptionAllowRemoteSharedDevices'
/>
</div>
<div className='fieldDescription'>
{globalize.translate('OptionAllowRemoteSharedDevicesHelp')}
</div>
</div>
<h2 className='checkboxListLabel'>
{globalize.translate('Other')}
</h2>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
className='chkEnableDownloading'
title='OptionAllowContentDownload'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionAllowContentDownloadHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
<CheckBoxElement
className='chkDisabled'
title='OptionDisableUser'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionDisableUserHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
<CheckBoxElement
className='chkIsHidden'
title='OptionHideUser'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionHideUserFromLoginHelp')}
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer' id='fldLoginAttemptsBeforeLockout'>
<Input
type='number'
id='txtLoginAttemptsBeforeLockout'
label={globalize.translate('LabelUserLoginAttemptsBeforeLockout')}
min={-1} step={1}
/>
<div className='fieldDescription'>
{globalize.translate('OptionLoginAttemptsBeforeLockout')}
</div>
<div className='fieldDescription'>
{globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')}
</div>
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer' id='fldMaxActiveSessions'>
<Input
type='number'
id='txtMaxActiveSessions'
label={globalize.translate('LabelUserMaxActiveSessions')}
min={0} step={1}
/>
<div className='fieldDescription'>
{globalize.translate('OptionMaxActiveSessions')}
</div>
<div className='fieldDescription'>
{globalize.translate('OptionMaxActiveSessionsHelp')}
</div>
</div>
</div>
<br />
<div>
<Button
type='submit'
className='raised button-submit block'
title={globalize.translate('Save')}
/>
<Button
type='button'
id='btnCancel'
className='raised button-cancel block'
title={globalize.translate('ButtonCancel')}
/>
</div>
</form>
<div ref={element}>
<div
className='lnkEditUserPreferencesContainer'
style={{ paddingBottom: '1em' }}
>
<LinkButton className='lnkEditUserPreferences button-link' href={userDto?.Id ? `mypreferencesmenu?userId=${userDto.Id}` : undefined}>
{globalize.translate('ButtonEditOtherUserPreferences')}
</LinkButton>
</div>
</Page>
<form className='editUserProfileForm'>
<div className='disabledUserBanner hide'>
<div className='btn btnDarkAccent btnStatic'>
<div>
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
</div>
<div style={{ marginTop: 5 }}>
{globalize.translate('MessageReenableUser')}
</div>
</div>
</div>
<div id='fldUserName' className='inputContainer'>
<Input
type='text'
id='txtUserName'
label={globalize.translate('LabelName')}
required
/>
</div>
<div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement
id='selectLoginProvider'
label='LabelAuthProvider'
>
{optionLoginProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('AuthProviderHelp')}
</div>
</div>
<div className='selectContainer fldSelectPasswordResetProvider hide'>
<SelectElement
id='selectPasswordResetProvider'
label='LabelPasswordResetProvider'
>
{optionPasswordResetProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('PasswordResetProviderHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
<CheckBoxElement
className='chkRemoteAccess'
title='AllowRemoteAccess'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('AllowRemoteAccessHelp')}
</div>
</div>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkIsAdmin'
title='OptionAllowUserToManageServer'
/>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableCollectionManagement'
title='AllowCollectionManagement'
/>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableSubtitleManagement'
title='AllowSubtitleManagement'
/>
<div id='featureAccessFields' className='verticalSection'>
<h2 className='paperListLabel'>
{globalize.translate('HeaderFeatureAccess')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableLiveTvAccess'
title='OptionAllowBrowsingLiveTv'
/>
<CheckBoxElement
className='chkManageLiveTv'
title='OptionAllowManageLiveTv'
/>
</div>
</div>
<div className='verticalSection'>
<h2 className='paperListLabel'>
{globalize.translate('HeaderPlayback')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableMediaPlayback'
title='OptionAllowMediaPlayback'
/>
<CheckBoxElement
className='chkEnableAudioPlaybackTranscoding'
title='OptionAllowAudioPlaybackTranscoding'
/>
<CheckBoxElement
className='chkEnableVideoPlaybackTranscoding'
title='OptionAllowVideoPlaybackTranscoding'
/>
<CheckBoxElement
className='chkEnableVideoPlaybackRemuxing'
title='OptionAllowVideoPlaybackRemuxing'
/>
<CheckBoxElement
className='chkForceRemoteSourceTranscoding'
title='OptionForceRemoteSourceTranscoding'
/>
</div>
<div className='fieldDescription'>
{globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')}
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer'>
<Input
type='number'
id='txtRemoteClientBitrateLimit'
label={globalize.translate('LabelRemoteClientBitrateLimit')}
inputMode='decimal'
pattern='[0-9]*(.[0-9]+)?'
min='0'
step='.25'
/>
<div className='fieldDescription'>
{globalize.translate('LabelRemoteClientBitrateLimitHelp')}
</div>
<div className='fieldDescription'>
{globalize.translate('LabelUserRemoteClientBitrateLimitHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectSyncPlayAccess'>
<SelectElement
id='selectSyncPlayAccess'
label='LabelSyncPlayAccess'
>
{optionSyncPlayAccess()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('SyncPlayAccessHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<h2 className='checkboxListLabel' style={{ marginBottom: '1em' }}>
{globalize.translate('HeaderAllowMediaDeletionFrom')}
</h2>
<div className='checkboxList paperList checkboxList-paperList'>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableDeleteAllFolders'
title='AllLibraries'
/>
<div className='deleteAccess'>
{deleteFoldersAccess.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</div>
</div>
</div>
<div className='verticalSection'>
<h2 className='checkboxListLabel'>
{globalize.translate('HeaderRemoteControl')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableRemoteControlOtherUsers'
title='OptionAllowRemoteControlOthers'
/>
<CheckBoxElement
className='chkRemoteControlSharedDevices'
title='OptionAllowRemoteSharedDevices'
/>
</div>
<div className='fieldDescription'>
{globalize.translate('OptionAllowRemoteSharedDevicesHelp')}
</div>
</div>
<h2 className='checkboxListLabel'>
{globalize.translate('Other')}
</h2>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
className='chkEnableDownloading'
title='OptionAllowContentDownload'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionAllowContentDownloadHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
<CheckBoxElement
className='chkDisabled'
title='OptionDisableUser'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionDisableUserHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
<CheckBoxElement
className='chkIsHidden'
title='OptionHideUser'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionHideUserFromLoginHelp')}
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer' id='fldLoginAttemptsBeforeLockout'>
<Input
type='number'
id='txtLoginAttemptsBeforeLockout'
label={globalize.translate('LabelUserLoginAttemptsBeforeLockout')}
min={-1} step={1}
/>
<div className='fieldDescription'>
{globalize.translate('OptionLoginAttemptsBeforeLockout')}
</div>
<div className='fieldDescription'>
{globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')}
</div>
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer' id='fldMaxActiveSessions'>
<Input
type='number'
id='txtMaxActiveSessions'
label={globalize.translate('LabelUserMaxActiveSessions')}
min={0} step={1}
/>
<div className='fieldDescription'>
{globalize.translate('OptionMaxActiveSessions')}
</div>
<div className='fieldDescription'>
{globalize.translate('OptionMaxActiveSessionsHelp')}
</div>
</div>
</div>
<br />
<div>
<Button
type='submit'
className='raised button-submit block'
title={globalize.translate('Save')}
/>
<Button
type='button'
id='btnCancel'
className='raised button-cancel block'
title={globalize.translate('ButtonCancel')}
/>
</div>
</form>
</div>
);
};
export default UserEdit;
export default Profile;
@@ -0,0 +1,6 @@
export enum UserTab {
Profile = 'profile',
Access = 'access',
ParentalControl = 'parentalcontrol',
Password = 'password'
}
+1 -4
View File
@@ -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 }
];
+1 -1
View File
@@ -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');
+72
View File
@@ -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 <Loading />;
return (
<Page
id='usersEditPage'
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
{isError ? (
<Alert
severity='error'
sx={{ marginBottom: 2 }}
>
{globalize.translate('UsersEditPageError')}
</Alert>
) : (
<Stack spacing={2}>
<Typography variant='h1'>{user.Name}</Typography>
<Tabs value={tab} onChange={handleTabChange}>
<Tab label={globalize.translate('Profile')} value={UserTab.Profile} />
<Tab label={globalize.translate('TabAccess')} value={UserTab.Access} />
<Tab label={globalize.translate('TabParentalControl')} value={UserTab.ParentalControl} />
<Tab label={globalize.translate('HeaderPassword')} value={UserTab.Password} />
</Tabs>
{tab == UserTab.Profile && (
<Profile userDto={user} />
)}
{tab == UserTab.Access && (
<Access userId={user.Id || ''} />
)}
{tab == UserTab.ParentalControl && (
<ParentalControl userId={user.Id || ''} />
)}
{tab == UserTab.Password && (
<Password user={user} />
)}
</Stack>
)}
</Box>
</Page>
);
};
Component.displayName = 'UsersEditPage';
+4 -3
View File
@@ -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':
@@ -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 <Loading />;
}
return (
<Page
id='userPasswordPage'
className='mainAnimatedPage type-interior userPasswordPage'
>
<div className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={user?.Name || undefined}
/>
</div>
<SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'>
<UserPasswordForm
user={user}
/>
</div>
</div>
</Page>
);
};
export default UserPassword;
@@ -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<IProps> = ({ 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 (
<div
data-role='controlgroup'
data-type='horizontal'
className='localnav'>
<LinkButton
href='#'
data-role='button'
className={activeTab === 'useredit' ? 'ui-btn-active' : ''}
onClick={onClickProfile}>
{globalize.translate('Profile')}
</LinkButton>
<LinkButton
href='#'
data-role='button'
className={activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}
onClick={onClickAccess}>
{globalize.translate('TabAccess')}
</LinkButton>
<LinkButton
href='#'
data-role='button'
className={activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}
onClick={onClickParentalControl}>
{globalize.translate('TabParentalControl')}
</LinkButton>
<LinkButton
href='#'
data-role='button'
className={activeTab === 'userpassword' ? 'ui-btn-active' : ''}
onClick={clickPassword}>
{globalize.translate('HeaderPassword')}
</LinkButton>
</div>
);
};
export default SectionTabs;
@@ -57,7 +57,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
<div className='cardPadder cardPadder-square'></div>
<LinkButton
className='cardContent'
href={`#/dashboard/users/profile?userId=${user.Id}`}>
href={`#/dashboard/users/${user.Id}/profile`}>
{renderImgUrl}
</LinkButton>
</div>
+1
View File
@@ -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}",