mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-09-03 03:50:02 +03:00
Merge pull request #7497 from viown/move-users-to-tabs
Move users edit page to tabs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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'],
|
||||
|
||||
+85
-101
@@ -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;
|
||||
+126
-141
@@ -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;
|
||||
+316
-343
@@ -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'
|
||||
}
|
||||
@@ -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 }
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
|
||||
@@ -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}",
|
||||
|
||||
Reference in New Issue
Block a user