mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-17 19:08:18 -07:00
Convert userPasswordPage & UserImagePage to react
This commit is contained in:
parent
4a8806e1f6
commit
2aa41f8a33
283
src/components/dashboard/users/UserPasswordForm.tsx
Normal file
283
src/components/dashboard/users/UserPasswordForm.tsx
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
import Dashboard from '../../../scripts/clientUtils';
|
||||||
|
import globalize from '../../../scripts/globalize';
|
||||||
|
import LibraryMenu from '../../../scripts/libraryMenu';
|
||||||
|
import confirm from '../../confirm/confirm';
|
||||||
|
import loading from '../../loading/loading';
|
||||||
|
import toast from '../../toast/toast';
|
||||||
|
import ButtonElement from './ButtonElement';
|
||||||
|
import CheckBoxElement from './CheckBoxElement';
|
||||||
|
import InputElement from './InputElement';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||||
|
const element = useRef(null);
|
||||||
|
|
||||||
|
const loadUser = (Id) => {
|
||||||
|
window.ApiClient.getUser(Id).then(function (user) {
|
||||||
|
Dashboard.getCurrentUser().then(function (loggedInUser) {
|
||||||
|
LibraryMenu.setTitle(user.Name);
|
||||||
|
|
||||||
|
let showPasswordSection = true;
|
||||||
|
let showLocalAccessSection = false;
|
||||||
|
|
||||||
|
if (user.ConnectLinkType == 'Guest') {
|
||||||
|
element.current?.querySelector('.localAccessSection').classList.add('hide');
|
||||||
|
showPasswordSection = false;
|
||||||
|
} else if (user.HasConfiguredPassword) {
|
||||||
|
element.current?.querySelector('.btnResetPassword').classList.remove('hide');
|
||||||
|
element.current?.querySelector('#fldCurrentPassword').classList.remove('hide');
|
||||||
|
showLocalAccessSection = true;
|
||||||
|
} else {
|
||||||
|
element.current?.querySelector('.btnResetPassword').classList.add('hide');
|
||||||
|
element.current?.querySelector('#fldCurrentPassword').classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPasswordSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
||||||
|
element.current?.querySelector('.passwordSection').classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
element.current?.querySelector('.passwordSection').classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLocalAccessSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
||||||
|
element.current?.querySelector('.localAccessSection').classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
element.current?.querySelector('.localAccessSection').classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
const txtEasyPassword = element.current?.querySelector('#txtEasyPassword');
|
||||||
|
txtEasyPassword.value = '';
|
||||||
|
|
||||||
|
if (user.HasConfiguredEasyPassword) {
|
||||||
|
txtEasyPassword.placeholder = '******';
|
||||||
|
element.current?.querySelector('.btnResetEasyPassword').classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
txtEasyPassword.removeAttribute('placeholder');
|
||||||
|
txtEasyPassword.placeholder = '';
|
||||||
|
element.current?.querySelector('.btnResetEasyPassword').classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
element.current.querySelector('.chkEnableLocalEasyPassword').checked = user.Configuration.EnableLocalPassword;
|
||||||
|
|
||||||
|
import('../../autoFocuser').then(({default: autoFocuser}) => {
|
||||||
|
autoFocuser.autoFocus(element.current);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
element.current.querySelector('#txtCurrentPassword').value = '';
|
||||||
|
element.current.querySelector('#txtNewPassword').value = '';
|
||||||
|
element.current.querySelector('#txtNewPasswordConfirm').value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUser(userId);
|
||||||
|
|
||||||
|
const onSubmit = (e) => {
|
||||||
|
const form = element.current;
|
||||||
|
|
||||||
|
if (form.querySelector('#txtNewPassword').value != form.querySelector('#txtNewPasswordConfirm').value) {
|
||||||
|
toast(globalize.translate('PasswordMatchError'));
|
||||||
|
} else {
|
||||||
|
loading.show();
|
||||||
|
savePassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const savePassword = () => {
|
||||||
|
let currentPassword = element.current?.querySelector('#txtCurrentPassword').value;
|
||||||
|
const newPassword = element.current?.querySelector('#txtNewPassword').value;
|
||||||
|
|
||||||
|
if (element.current?.querySelector('#fldCurrentPassword').classList.contains('hide')) {
|
||||||
|
// Firefox does not respect autocomplete=off, so clear it if the field is supposed to be hidden (and blank)
|
||||||
|
// This should only happen when user.HasConfiguredPassword is false, but this information is not passed on
|
||||||
|
currentPassword = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ApiClient.updateUserPassword(userId, currentPassword, newPassword).then(function () {
|
||||||
|
loading.hide();
|
||||||
|
toast(globalize.translate('PasswordSaved'));
|
||||||
|
|
||||||
|
loadUser(userId);
|
||||||
|
}, function () {
|
||||||
|
loading.hide();
|
||||||
|
Dashboard.alert({
|
||||||
|
title: globalize.translate('HeaderLoginFailure'),
|
||||||
|
message: globalize.translate('MessageInvalidUser')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLocalAccessSubmit = (e) => {
|
||||||
|
loading.show();
|
||||||
|
saveEasyPassword();
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEasyPassword = () => {
|
||||||
|
const easyPassword = element.current?.querySelector('#txtEasyPassword').value;
|
||||||
|
|
||||||
|
if (easyPassword) {
|
||||||
|
window.ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
|
||||||
|
onEasyPasswordSaved(userId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onEasyPasswordSaved(userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEasyPasswordSaved = (Id) => {
|
||||||
|
window.ApiClient.getUser(Id).then(function (user) {
|
||||||
|
user.Configuration.EnableLocalPassword = element.current?.querySelector('.chkEnableLocalEasyPassword').checked;
|
||||||
|
window.ApiClient.updateUserConfiguration(user.Id, user.Configuration).then(function () {
|
||||||
|
loading.hide();
|
||||||
|
toast(globalize.translate('SettingsSaved'));
|
||||||
|
|
||||||
|
loadUser(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetEasyPassword = () => {
|
||||||
|
const msg = globalize.translate('PinCodeResetConfirmation');
|
||||||
|
|
||||||
|
confirm(msg, globalize.translate('HeaderPinCodeReset')).then(function () {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.resetEasyPassword(userId).then(function () {
|
||||||
|
loading.hide();
|
||||||
|
Dashboard.alert({
|
||||||
|
message: globalize.translate('PinCodeResetComplete'),
|
||||||
|
title: globalize.translate('HeaderPinCodeReset')
|
||||||
|
});
|
||||||
|
loadUser(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPassword = () => {
|
||||||
|
const msg = globalize.translate('PasswordResetConfirmation');
|
||||||
|
confirm(msg, globalize.translate('ResetPassword')).then(function () {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.resetUserPassword(userId).then(function () {
|
||||||
|
loading.hide();
|
||||||
|
Dashboard.alert({
|
||||||
|
message: globalize.translate('PasswordResetComplete'),
|
||||||
|
title: globalize.translate('ResetPassword')
|
||||||
|
});
|
||||||
|
loadUser(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
element?.current?.querySelector('.updatePasswordForm').addEventListener('submit', onSubmit);
|
||||||
|
element?.current?.querySelector('.localAccessForm').addEventListener('submit', onLocalAccessSubmit);
|
||||||
|
|
||||||
|
element?.current?.querySelector('.btnResetEasyPassword').addEventListener('click', resetEasyPassword);
|
||||||
|
element?.current?.querySelector('.btnResetPassword').addEventListener('click', resetPassword);
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<form
|
||||||
|
className='updatePasswordForm passwordSection hide'
|
||||||
|
style={{margin: '0 auto 2em'}}
|
||||||
|
>
|
||||||
|
<div className='detailSection'>
|
||||||
|
<div id='fldCurrentPassword' className='inputContainer hide'>
|
||||||
|
<InputElement
|
||||||
|
type='password'
|
||||||
|
id='txtCurrentPassword'
|
||||||
|
label='LabelCurrentPassword'
|
||||||
|
options={'autoComplete="off"'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='password'
|
||||||
|
id='txtNewPassword'
|
||||||
|
label='LabelNewPassword'
|
||||||
|
options={'autoComplete="off"'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='password'
|
||||||
|
id='txtNewPasswordConfirm'
|
||||||
|
label='LabelNewPasswordConfirm'
|
||||||
|
options={'autoComplete="off"'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
<ButtonElement
|
||||||
|
type='submit'
|
||||||
|
className='raised button-submit block'
|
||||||
|
title='Save'
|
||||||
|
/>
|
||||||
|
<ButtonElement
|
||||||
|
type='button'
|
||||||
|
className='raised btnResetPassword button-cancel block hide'
|
||||||
|
title='ResetPassword'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br />
|
||||||
|
<form
|
||||||
|
className='localAccessForm localAccessSection'
|
||||||
|
style={{margin: '0 auto'}}
|
||||||
|
>
|
||||||
|
<div className='detailSection'>
|
||||||
|
<div className='detailSectionHeader'>
|
||||||
|
{globalize.translate('HeaderEasyPinCode')}
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
{globalize.translate('EasyPasswordHelp')}
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div className='inputContainer'>
|
||||||
|
<InputElement
|
||||||
|
type='number'
|
||||||
|
id='txtEasyPassword'
|
||||||
|
label='LabelEasyPinCode'
|
||||||
|
options={'autoComplete="off" pattern="[0-9]*" step="1" maxlength="5"'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div className='checkboxContainer checkboxContainer-withDescription'>
|
||||||
|
<CheckBoxElement
|
||||||
|
type='checkbox'
|
||||||
|
className='chkEnableLocalEasyPassword'
|
||||||
|
title='LabelInNetworkSignInWithEasyPassword'
|
||||||
|
/>
|
||||||
|
<div className='fieldDescription checkboxFieldDescription'>
|
||||||
|
{globalize.translate('LabelInNetworkSignInWithEasyPasswordHelp')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ButtonElement
|
||||||
|
type='submit'
|
||||||
|
className='raised button-submit block'
|
||||||
|
title='Save'
|
||||||
|
/>
|
||||||
|
<ButtonElement
|
||||||
|
type='button'
|
||||||
|
className='raised btnResetEasyPassword button-cancel block hide'
|
||||||
|
title='ButtonResetEasyPassword'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPasswordForm;
|
165
src/components/pages/UserImagePage.tsx
Normal file
165
src/components/pages/UserImagePage.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
import Dashboard from '../../scripts/clientUtils';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import LibraryMenu from '../../scripts/libraryMenu';
|
||||||
|
import { appHost } from '../apphost';
|
||||||
|
import confirm from '../confirm/confirm';
|
||||||
|
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||||
|
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
|
||||||
|
import loading from '../loading/loading';
|
||||||
|
import toast from '../toast/toast';
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserImagePage: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||||
|
const [ userName, setUserName ] = useState('');
|
||||||
|
|
||||||
|
const element = useRef(null);
|
||||||
|
|
||||||
|
const reloadUser = (Id) => {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.getUser(Id).then(function (user) {
|
||||||
|
setUserName(user.Name);
|
||||||
|
LibraryMenu.setTitle(user.Name);
|
||||||
|
|
||||||
|
let imageUrl = 'assets/img/avatar.png';
|
||||||
|
if (user.PrimaryImageTag) {
|
||||||
|
imageUrl = window.ApiClient.getUserImageUrl(user.Id, {
|
||||||
|
tag: user.PrimaryImageTag,
|
||||||
|
type: 'Primary'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userImage = element.current?.querySelector('#image');
|
||||||
|
userImage.style.backgroundImage = 'url(' + imageUrl + ')';
|
||||||
|
|
||||||
|
Dashboard.getCurrentUser().then(function (loggedInUser) {
|
||||||
|
if (user.PrimaryImageTag) {
|
||||||
|
element.current?.querySelector('.btnAddImage').classList.add('hide');
|
||||||
|
element.current?.querySelector('.btnDeleteImage').classList.remove('hide');
|
||||||
|
} else if (appHost.supports('fileinput') && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
||||||
|
element.current?.querySelector('.btnDeleteImage').classList.add('hide');
|
||||||
|
element.current?.querySelector('.btnAddImage').classList.remove('hide');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loading.hide();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadUser(userId);
|
||||||
|
|
||||||
|
const onFileReaderError = (evt) => {
|
||||||
|
loading.hide();
|
||||||
|
switch (evt.target.error.code) {
|
||||||
|
case evt.target.error.NOT_FOUND_ERR:
|
||||||
|
toast(globalize.translate('FileNotFound'));
|
||||||
|
break;
|
||||||
|
case evt.target.error.ABORT_ERR:
|
||||||
|
onFileReaderAbort();
|
||||||
|
break;
|
||||||
|
case evt.target.error.NOT_READABLE_ERR:
|
||||||
|
default:
|
||||||
|
toast(globalize.translate('FileReadError'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFileReaderAbort = () => {
|
||||||
|
loading.hide();
|
||||||
|
toast(globalize.translate('FileReadCancelled'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFiles = (evt) => {
|
||||||
|
const userImage = element?.current?.querySelector('#image');
|
||||||
|
const file = evt.target.files[0];
|
||||||
|
|
||||||
|
if (!file || !file.type.match('image.*')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader: FileReader = new FileReader();
|
||||||
|
reader.onerror = onFileReaderError;
|
||||||
|
reader.onabort = onFileReaderAbort;
|
||||||
|
reader.onload = () => {
|
||||||
|
userImage.style.backgroundImage = 'url(' + reader.result + ')';
|
||||||
|
window.ApiClient.uploadUserImage(userId, 'Primary', file).then(function () {
|
||||||
|
loading.hide();
|
||||||
|
reloadUser(userId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
element?.current?.querySelector('.btnDeleteImage').addEventListener('click', function () {
|
||||||
|
confirm(
|
||||||
|
globalize.translate('DeleteImageConfirmation'),
|
||||||
|
globalize.translate('DeleteImage')
|
||||||
|
).then(function () {
|
||||||
|
loading.show();
|
||||||
|
window.ApiClient.deleteUserImage(userId, 'primary').then(function () {
|
||||||
|
loading.hide();
|
||||||
|
reloadUser(userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
element?.current?.querySelector('.btnAddImage').addEventListener('click', function () {
|
||||||
|
element?.current?.querySelector('#uploadImage').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
element?.current?.querySelector('#uploadImage').addEventListener('change', function (evt) {
|
||||||
|
setFiles(evt);
|
||||||
|
});
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={element}>
|
||||||
|
<div className='padded-left padded-right padded-bottom-page'>
|
||||||
|
<div
|
||||||
|
className='readOnlyContent'
|
||||||
|
style={{margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center'}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{position: 'relative', display: 'inline-block', maxWidth: 200 }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id='uploadImage'
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
style={{position: 'absolute', right: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer'}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id='image'
|
||||||
|
style={{width: 200, height: 200, backgroundRepeat: 'no-repeat', backgroundPosition: 'center', borderRadius: '100%', backgroundSize: 'cover'}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{verticalAlign: 'top', margin: '1em 2em', display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
|
||||||
|
<h2 className='username' style={{margin: 0, fontSize: 'xx-large'}}>
|
||||||
|
{userName}
|
||||||
|
</h2>
|
||||||
|
<br />
|
||||||
|
<ButtonElement
|
||||||
|
type='button'
|
||||||
|
className='raised btnAddImage hide'
|
||||||
|
title='ButtonAddImage'
|
||||||
|
/>
|
||||||
|
<ButtonElement
|
||||||
|
type='button'
|
||||||
|
className='raised btnDeleteImage hide'
|
||||||
|
title='DeleteImage'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserPasswordForm
|
||||||
|
userId={userId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserImagePage;
|
47
src/components/pages/UserPasswordPage.tsx
Normal file
47
src/components/pages/UserPasswordPage.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||||
|
import { appRouter } from '../appRouter';
|
||||||
|
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||||
|
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||||
|
import UserPasswordForm from '../dashboard/users/UserPasswordForm';
|
||||||
|
|
||||||
|
const UserPasswordPage: FunctionComponent = () => {
|
||||||
|
const userId = appRouter.param('userId');
|
||||||
|
const [ userName, setUserName ] = useState('');
|
||||||
|
|
||||||
|
const loadUser = (Id) => {
|
||||||
|
window.ApiClient.getUser(Id).then(function (user) {
|
||||||
|
setUserName(user.Name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadUser(userId);
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='content-primary'>
|
||||||
|
<div className='verticalSection'>
|
||||||
|
<div className='sectionTitleContainer flex align-items-center'>
|
||||||
|
<h2 className='sectionTitle username'>
|
||||||
|
{userName}
|
||||||
|
</h2>
|
||||||
|
<SectionTitleLinkElement
|
||||||
|
className='raised button-alt headerHelpButton'
|
||||||
|
title='Help'
|
||||||
|
url='https://docs.jellyfin.org/general/server/users/'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SectionTabs activeTab='userpassword'/>
|
||||||
|
<div className='readOnlyContent'>
|
||||||
|
<UserPasswordForm
|
||||||
|
userId={userId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPasswordPage;
|
@ -1,72 +1,3 @@
|
|||||||
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
|
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
|
||||||
<div>
|
|
||||||
<div class="content-primary">
|
|
||||||
<div class="verticalSection">
|
|
||||||
<div class="sectionTitleContainer flex align-items-center">
|
|
||||||
<h2 class="sectionTitle username"></h2>
|
|
||||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://docs.jellyfin.org/general/server/users/">${Help}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
|
|
||||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
|
|
||||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
|
|
||||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
|
|
||||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);" class="ui-btn-active">${HeaderPassword}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="readOnlyContent">
|
|
||||||
<form class="updatePasswordForm passwordSection hide" style="margin: 0 auto 2em;">
|
|
||||||
<div class="detailSection">
|
|
||||||
<div id="fldCurrentPassword" class="inputContainer hide">
|
|
||||||
<input is="emby-input" type="password" id="txtCurrentPassword" label="${LabelCurrentPassword}" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="password" id="txtNewPassword" label="${LabelNewPassword}" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="password" id="txtNewPasswordConfirm" label="${LabelNewPasswordConfirm}" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button>
|
|
||||||
<button is="emby-button" type="button" id="btnResetPassword" class="raised button-cancel block hide">
|
|
||||||
<span>${ResetPassword}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<br />
|
|
||||||
<form class="localAccessForm localAccessSection" style="margin: 0 auto;">
|
|
||||||
<div class="detailSection">
|
|
||||||
<div class="detailSectionHeader">
|
|
||||||
${HeaderEasyPinCode}
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div>${EasyPasswordHelp}</div>
|
|
||||||
<br />
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="number" id="txtEasyPassword" label="${LabelEasyPinCode}" autocomplete="off" pattern="[0-9]*" step="1" maxlength="5" />
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkEnableLocalEasyPassword" />
|
|
||||||
<span>${LabelInNetworkSignInWithEasyPassword}</span>
|
|
||||||
</label>
|
|
||||||
<div class="fieldDescription checkboxFieldDescription">${LabelInNetworkSignInWithEasyPasswordHelp}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
|
||||||
<span>${Save}</span>
|
|
||||||
</button>
|
|
||||||
<button is="emby-button" type="button" id="btnResetEasyPassword" class="raised button-cancel block hide">
|
|
||||||
<span>${ButtonResetEasyPassword}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,182 +0,0 @@
|
|||||||
import loading from '../../../components/loading/loading';
|
|
||||||
import libraryMenu from '../../../scripts/libraryMenu';
|
|
||||||
import globalize from '../../../scripts/globalize';
|
|
||||||
import '../../../elements/emby-button/emby-button';
|
|
||||||
import Dashboard from '../../../scripts/clientUtils';
|
|
||||||
import toast from '../../../components/toast/toast';
|
|
||||||
import confirm from '../../../components/confirm/confirm';
|
|
||||||
|
|
||||||
/* eslint-disable indent */
|
|
||||||
|
|
||||||
function loadUser(page, params) {
|
|
||||||
const userid = params.userId;
|
|
||||||
ApiClient.getUser(userid).then(function (user) {
|
|
||||||
Dashboard.getCurrentUser().then(function (loggedInUser) {
|
|
||||||
libraryMenu.setTitle(user.Name);
|
|
||||||
page.querySelector('.username').innerText = user.Name;
|
|
||||||
let showPasswordSection = true;
|
|
||||||
let showLocalAccessSection = false;
|
|
||||||
|
|
||||||
if (user.ConnectLinkType == 'Guest') {
|
|
||||||
page.querySelector('.localAccessSection').classList.add('hide');
|
|
||||||
showPasswordSection = false;
|
|
||||||
} else if (user.HasConfiguredPassword) {
|
|
||||||
page.querySelector('#btnResetPassword').classList.remove('hide');
|
|
||||||
page.querySelector('#fldCurrentPassword').classList.remove('hide');
|
|
||||||
showLocalAccessSection = true;
|
|
||||||
} else {
|
|
||||||
page.querySelector('#btnResetPassword').classList.add('hide');
|
|
||||||
page.querySelector('#fldCurrentPassword').classList.add('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showPasswordSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
|
||||||
page.querySelector('.passwordSection').classList.remove('hide');
|
|
||||||
} else {
|
|
||||||
page.querySelector('.passwordSection').classList.add('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showLocalAccessSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
|
||||||
page.querySelector('.localAccessSection').classList.remove('hide');
|
|
||||||
} else {
|
|
||||||
page.querySelector('.localAccessSection').classList.add('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
const txtEasyPassword = page.querySelector('#txtEasyPassword');
|
|
||||||
txtEasyPassword.value = '';
|
|
||||||
|
|
||||||
if (user.HasConfiguredEasyPassword) {
|
|
||||||
txtEasyPassword.placeholder = '******';
|
|
||||||
page.querySelector('#btnResetEasyPassword').classList.remove('hide');
|
|
||||||
} else {
|
|
||||||
txtEasyPassword.removeAttribute('placeholder');
|
|
||||||
txtEasyPassword.placeholder = '';
|
|
||||||
page.querySelector('#btnResetEasyPassword').classList.add('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
page.querySelector('.chkEnableLocalEasyPassword').checked = user.Configuration.EnableLocalPassword;
|
|
||||||
|
|
||||||
import('../../../components/autoFocuser').then(({default: autoFocuser}) => {
|
|
||||||
autoFocuser.autoFocus(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
page.querySelector('#txtCurrentPassword').value = '';
|
|
||||||
page.querySelector('#txtNewPassword').value = '';
|
|
||||||
page.querySelector('#txtNewPasswordConfirm').value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (view, params) {
|
|
||||||
function saveEasyPassword() {
|
|
||||||
const userId = params.userId;
|
|
||||||
const easyPassword = view.querySelector('#txtEasyPassword').value;
|
|
||||||
|
|
||||||
if (easyPassword) {
|
|
||||||
ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
|
|
||||||
onEasyPasswordSaved(userId);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onEasyPasswordSaved(userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEasyPasswordSaved(userId) {
|
|
||||||
ApiClient.getUser(userId).then(function (user) {
|
|
||||||
user.Configuration.EnableLocalPassword = view.querySelector('.chkEnableLocalEasyPassword').checked;
|
|
||||||
ApiClient.updateUserConfiguration(user.Id, user.Configuration).then(function () {
|
|
||||||
loading.hide();
|
|
||||||
toast(globalize.translate('SettingsSaved'));
|
|
||||||
|
|
||||||
loadUser(view, params);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePassword() {
|
|
||||||
const userId = params.userId;
|
|
||||||
let currentPassword = view.querySelector('#txtCurrentPassword').value;
|
|
||||||
const newPassword = view.querySelector('#txtNewPassword').value;
|
|
||||||
|
|
||||||
if (view.querySelector('#fldCurrentPassword').classList.contains('hide')) {
|
|
||||||
// Firefox does not respect autocomplete=off, so clear it if the field is supposed to be hidden (and blank)
|
|
||||||
// This should only happen when user.HasConfiguredPassword is false, but this information is not passed on
|
|
||||||
currentPassword = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiClient.updateUserPassword(userId, currentPassword, newPassword).then(function () {
|
|
||||||
loading.hide();
|
|
||||||
toast(globalize.translate('PasswordSaved'));
|
|
||||||
|
|
||||||
loadUser(view, params);
|
|
||||||
}, function () {
|
|
||||||
loading.hide();
|
|
||||||
Dashboard.alert({
|
|
||||||
title: globalize.translate('HeaderLoginFailure'),
|
|
||||||
message: globalize.translate('MessageInvalidUser')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmit(e) {
|
|
||||||
const form = this;
|
|
||||||
|
|
||||||
if (form.querySelector('#txtNewPassword').value != form.querySelector('#txtNewPasswordConfirm').value) {
|
|
||||||
toast(globalize.translate('PasswordMatchError'));
|
|
||||||
} else {
|
|
||||||
loading.show();
|
|
||||||
savePassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onLocalAccessSubmit(e) {
|
|
||||||
loading.show();
|
|
||||||
saveEasyPassword();
|
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPassword() {
|
|
||||||
const msg = globalize.translate('PasswordResetConfirmation');
|
|
||||||
confirm(msg, globalize.translate('ResetPassword')).then(function () {
|
|
||||||
const userId = params.userId;
|
|
||||||
loading.show();
|
|
||||||
ApiClient.resetUserPassword(userId).then(function () {
|
|
||||||
loading.hide();
|
|
||||||
Dashboard.alert({
|
|
||||||
message: globalize.translate('PasswordResetComplete'),
|
|
||||||
title: globalize.translate('ResetPassword')
|
|
||||||
});
|
|
||||||
loadUser(view, params);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetEasyPassword() {
|
|
||||||
const msg = globalize.translate('PinCodeResetConfirmation');
|
|
||||||
|
|
||||||
confirm(msg, globalize.translate('HeaderPinCodeReset')).then(function () {
|
|
||||||
const userId = params.userId;
|
|
||||||
loading.show();
|
|
||||||
ApiClient.resetEasyPassword(userId).then(function () {
|
|
||||||
loading.hide();
|
|
||||||
Dashboard.alert({
|
|
||||||
message: globalize.translate('PinCodeResetComplete'),
|
|
||||||
title: globalize.translate('HeaderPinCodeReset')
|
|
||||||
});
|
|
||||||
loadUser(view, params);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
view.querySelector('.updatePasswordForm').addEventListener('submit', onSubmit);
|
|
||||||
view.querySelector('.localAccessForm').addEventListener('submit', onLocalAccessSubmit);
|
|
||||||
view.querySelector('#btnResetEasyPassword').addEventListener('click', resetEasyPassword);
|
|
||||||
view.querySelector('#btnResetPassword').addEventListener('click', resetPassword);
|
|
||||||
view.addEventListener('viewshow', function () {
|
|
||||||
loadUser(view, params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-enable indent */
|
|
@ -1,69 +1,3 @@
|
|||||||
<div id="userImagePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
|
<div id="userImagePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
|
||||||
<div class="padded-left padded-right padded-bottom-page">
|
|
||||||
<div class="readOnlyContent" style="margin: 0 auto; padding: 0 1em;">
|
|
||||||
<div style="position:relative;display:inline-block;max-width:200px;">
|
|
||||||
<input id="uploadImage" type="file" accept="image/*" style="position:absolute;right:0;width:100%;height:100%;opacity:0;" />
|
|
||||||
<div id="image" style="width:200px;height:200px;background-repeat:no-repeat;background-position:center;border-radius:100%;background-size:cover;"></div>
|
|
||||||
</div>
|
|
||||||
<div style="vertical-align:top;margin:1em 2em;display:inline-block;">
|
|
||||||
<h2 class="username" style="margin:0;font-size:xx-large;"></h2>
|
|
||||||
<br/>
|
|
||||||
<button is="emby-button" type="button" class="raised hide" id="btnAddImage">
|
|
||||||
<span>${ButtonAddImage}</span>
|
|
||||||
</button>
|
|
||||||
<button is="emby-button" type="button" class="raised hide" id="btnDeleteImage">
|
|
||||||
<span>${DeleteImage}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form class="updatePasswordForm passwordSection userProfileSettingsForm hide" style="margin: 3em auto 0;">
|
|
||||||
<div class="verticalSection">
|
|
||||||
<h2 class="sectionTitle">
|
|
||||||
${HeaderPassword}
|
|
||||||
</h2>
|
|
||||||
<div id="fldCurrentPassword" class="inputContainer hide">
|
|
||||||
<input is="emby-input" type="password" id="txtCurrentPassword" label="${LabelCurrentPassword}" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="password" id="txtNewPassword" label="${LabelNewPassword}" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="password" id="txtNewPasswordConfirm" label="${LabelNewPasswordConfirm}" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
|
||||||
<span>${Save}</span>
|
|
||||||
</button>
|
|
||||||
<button is="emby-button" type="button" id="btnResetPassword" class="raised cancel block hide">
|
|
||||||
<span>${ResetPassword}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form class="localAccessForm localAccessSection userProfileSettingsForm hide" style="margin: 3em auto 0;">
|
|
||||||
<div class="verticalSection">
|
|
||||||
<h2 class="sectionTitle">${HeaderEasyPinCode}</h2>
|
|
||||||
<div>${EasyPasswordHelp}</div>
|
|
||||||
<br />
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="number" id="txtEasyPassword" label="${LabelEasyPinCode}" autocomplete="off" pattern="[0-9]*" step="1" maxlength="5" />
|
|
||||||
</div>
|
|
||||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" is="emby-checkbox" class="chkEnableLocalEasyPassword" />
|
|
||||||
<span>${LabelInNetworkSignInWithEasyPassword}</span>
|
|
||||||
</label>
|
|
||||||
<div class="fieldDescription checkboxFieldDescription">${LabelInNetworkSignInWithEasyPasswordHelp}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
|
||||||
<span>${Save}</span>
|
|
||||||
</button>
|
|
||||||
<button is="emby-button" type="button" id="btnResetEasyPassword" class="raised cancel block hide">
|
|
||||||
<span>${ButtonResetEasyPassword}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,104 +0,0 @@
|
|||||||
import UserPasswordPage from '../../dashboard/users/userpasswordpage';
|
|
||||||
import loading from '../../../components/loading/loading';
|
|
||||||
import libraryMenu from '../../../scripts/libraryMenu';
|
|
||||||
import { appHost } from '../../../components/apphost';
|
|
||||||
import globalize from '../../../scripts/globalize';
|
|
||||||
import '../../../elements/emby-button/emby-button';
|
|
||||||
import Dashboard from '../../../scripts/clientUtils';
|
|
||||||
import toast from '../../../components/toast/toast';
|
|
||||||
import confirm from '../../../components/confirm/confirm';
|
|
||||||
|
|
||||||
function reloadUser(page) {
|
|
||||||
const userId = getParameterByName('userId');
|
|
||||||
loading.show();
|
|
||||||
ApiClient.getUser(userId).then(function (user) {
|
|
||||||
page.querySelector('.username').innerText = user.Name;
|
|
||||||
libraryMenu.setTitle(user.Name);
|
|
||||||
|
|
||||||
let imageUrl = 'assets/img/avatar.png';
|
|
||||||
if (user.PrimaryImageTag) {
|
|
||||||
imageUrl = ApiClient.getUserImageUrl(user.Id, {
|
|
||||||
tag: user.PrimaryImageTag,
|
|
||||||
type: 'Primary'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userImage = page.querySelector('#image');
|
|
||||||
userImage.style.backgroundImage = 'url(' + imageUrl + ')';
|
|
||||||
|
|
||||||
Dashboard.getCurrentUser().then(function (loggedInUser) {
|
|
||||||
if (user.PrimaryImageTag) {
|
|
||||||
page.querySelector('#btnAddImage').classList.add('hide');
|
|
||||||
page.querySelector('#btnDeleteImage').classList.remove('hide');
|
|
||||||
} else if (appHost.supports('fileinput') && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
|
||||||
page.querySelector('#btnDeleteImage').classList.add('hide');
|
|
||||||
page.querySelector('#btnAddImage').classList.remove('hide');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
loading.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileReaderError(evt) {
|
|
||||||
loading.hide();
|
|
||||||
switch (evt.target.error.code) {
|
|
||||||
case evt.target.error.NOT_FOUND_ERR:
|
|
||||||
toast(globalize.translate('FileNotFound'));
|
|
||||||
break;
|
|
||||||
case evt.target.error.ABORT_ERR:
|
|
||||||
onFileReaderAbort();
|
|
||||||
break;
|
|
||||||
case evt.target.error.NOT_READABLE_ERR:
|
|
||||||
default:
|
|
||||||
toast(globalize.translate('FileReadError'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFileReaderAbort() {
|
|
||||||
loading.hide();
|
|
||||||
toast(globalize.translate('FileReadCancelled'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFiles(page, files) {
|
|
||||||
const userImage = page.querySelector('#image');
|
|
||||||
const file = files[0];
|
|
||||||
|
|
||||||
if (!file || !file.type.match('image.*')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onerror = onFileReaderError;
|
|
||||||
reader.onabort = onFileReaderAbort;
|
|
||||||
reader.onload = function (evt) {
|
|
||||||
userImage.style.backgroundImage = 'url(' + evt.target.result + ')';
|
|
||||||
const userId = getParameterByName('userId');
|
|
||||||
ApiClient.uploadUserImage(userId, 'Primary', file).then(function () {
|
|
||||||
loading.hide();
|
|
||||||
reloadUser(page);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (view, params) {
|
|
||||||
reloadUser(view);
|
|
||||||
new UserPasswordPage(view, params);
|
|
||||||
view.querySelector('#btnDeleteImage').addEventListener('click', function () {
|
|
||||||
confirm(globalize.translate('DeleteImageConfirmation'), globalize.translate('DeleteImage')).then(function () {
|
|
||||||
loading.show();
|
|
||||||
const userId = getParameterByName('userId');
|
|
||||||
ApiClient.deleteUserImage(userId, 'primary').then(function () {
|
|
||||||
loading.hide();
|
|
||||||
reloadUser(view);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
view.querySelector('#btnAddImage').addEventListener('click', function () {
|
|
||||||
view.querySelector('#uploadImage').click();
|
|
||||||
});
|
|
||||||
view.querySelector('#uploadImage').addEventListener('change', function (evt) {
|
|
||||||
setFiles(view, evt.target.files);
|
|
||||||
});
|
|
||||||
}
|
|
@ -81,7 +81,7 @@ import { appRouter } from '../components/appRouter';
|
|||||||
alias: '/myprofile.html',
|
alias: '/myprofile.html',
|
||||||
path: 'user/profile/index.html',
|
path: 'user/profile/index.html',
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
controller: 'user/profile/index'
|
pageComponent: 'UserImagePage'
|
||||||
});
|
});
|
||||||
|
|
||||||
defineRoute({
|
defineRoute({
|
||||||
@ -471,7 +471,7 @@ import { appRouter } from '../components/appRouter';
|
|||||||
alias: '/userpassword.html',
|
alias: '/userpassword.html',
|
||||||
path: 'dashboard/users/userpassword.html',
|
path: 'dashboard/users/userpassword.html',
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
controller: 'dashboard/users/userpasswordpage'
|
pageComponent: 'UserPasswordPage'
|
||||||
});
|
});
|
||||||
|
|
||||||
defineRoute({
|
defineRoute({
|
||||||
|
Loading…
Reference in New Issue
Block a user