1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #5226 from terite/terite-emby-linkbutton

use LinkButton instead of dangerouslySetInnerHTML
This commit is contained in:
Bill Thornton 2024-09-23 12:27:37 -04:00 committed by GitHub
commit 3a414a2da3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 87 additions and 151 deletions

View file

@ -9,7 +9,7 @@ import LibraryMenu from '../../../../scripts/libraryMenu';
import ButtonElement from '../../../../elements/ButtonElement'; import ButtonElement from '../../../../elements/ButtonElement';
import CheckBoxElement from '../../../../elements/CheckBoxElement'; import CheckBoxElement from '../../../../elements/CheckBoxElement';
import InputElement from '../../../../elements/InputElement'; import InputElement from '../../../../elements/InputElement';
import LinkEditUserPreferences from '../../../../components/dashboard/users/LinkEditUserPreferences'; import LinkButton from '../../../../elements/emby-button/LinkButton';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import SectionTabs from '../../../../components/dashboard/users/SectionTabs'; import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
import loading from '../../../../components/loading/loading'; import loading from '../../../../components/loading/loading';
@ -38,7 +38,7 @@ function onSaveComplete() {
const UserEdit = () => { const UserEdit = () => {
const [ searchParams ] = useSearchParams(); const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId'); const userId = searchParams.get('userId');
const [ userName, setUserName ] = useState(''); const [ userDto, setUserDto ] = useState<UserDto>();
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<NameIdPair[]>([]); const [ authProviders, setAuthProviders ] = useState<NameIdPair[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<NameIdPair[]>([]); const [ passwordResetProviders, setPasswordResetProviders ] = useState<NameIdPair[]>([]);
@ -147,10 +147,8 @@ const UserEdit = () => {
txtUserName.disabled = false; txtUserName.disabled = false;
txtUserName.removeAttribute('disabled'); txtUserName.removeAttribute('disabled');
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
LibraryMenu.setTitle(user.Name); LibraryMenu.setTitle(user.Name);
setUserName(user.Name || ''); setUserDto(user);
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || ''; (page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name || '';
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator; (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = !!user.Policy?.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled; (page.querySelector('.chkDisabled') as HTMLInputElement).checked = !!user.Policy?.IsDisabled;
@ -292,7 +290,7 @@ const UserEdit = () => {
<div ref={element} className='content-primary'> <div ref={element} className='content-primary'>
<div className='verticalSection'> <div className='verticalSection'>
<SectionTitleContainer <SectionTitleContainer
title={userName} title={userDto?.Name || ''}
url='https://jellyfin.org/docs/general/server/users/' url='https://jellyfin.org/docs/general/server/users/'
/> />
</div> </div>
@ -302,10 +300,9 @@ const UserEdit = () => {
className='lnkEditUserPreferencesContainer' className='lnkEditUserPreferencesContainer'
style={{ paddingBottom: '1em' }} style={{ paddingBottom: '1em' }}
> >
<LinkEditUserPreferences <LinkButton className='lnkEditUserPreferences button-link' href={userDto?.Id ? `mypreferencesmenu.html?userId=${userDto.Id}` : undefined}>
className= 'lnkEditUserPreferences button-link' {globalize.translate('ButtonEditOtherUserPreferences')}
title= 'ButtonEditOtherUserPreferences' </LinkButton>
/>
</div> </div>
<form className='editUserProfileForm'> <form className='editUserProfileForm'>
<div className='disabledUserBanner hide'> <div className='disabledUserBanner hide'>

View file

@ -1,30 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from 'lib/globalize';
type IProps = {
title?: string;
className?: string;
};
const createLinkElement = ({ className, title }: IProps) => ({
__html: `<a
is="emby-linkbutton"
class="${className}"
href='#'
>
${title}
</a>`
});
const LinkEditUserPreferences: FunctionComponent<IProps> = ({ className, title }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createLinkElement({
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default LinkEditUserPreferences;

View file

@ -1,49 +1,60 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from 'lib/globalize'; import globalize from 'lib/globalize';
import { navigate } from '../../../utils/dashboard';
import LinkButton from '../../../elements/emby-button/LinkButton';
type IProps = { type IProps = {
activeTab: string; activeTab: string;
}; };
const createLinkElement = (activeTab: string) => ({ function useNavigate(url: string): () => void {
__html: `<a href="#" return React.useCallback(() => {
is="emby-linkbutton" navigate(url, true).catch(err => {
data-role="button" console.warn('Error navigating to dashboard url', err);
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}" });
onclick="Dashboard.navigate('/dashboard/users/profile', true);"> }, [url]);
${globalize.translate('Profile')} }
</a>
<a href="#"
is="emby-linkbutton"
data-role="button"
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('/dashboard/users/access', true);">
${globalize.translate('TabAccess')}
</a>
<a href="#"
is="emby-linkbutton"
data-role="button"
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('/dashboard/users/parentalcontrol', true);">
${globalize.translate('TabParentalControl')}
</a>
<a href="#"
is="emby-linkbutton"
data-role="button"
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
onclick="Dashboard.navigate('/dashboard/users/password', true);">
${globalize.translate('HeaderPassword')}
</a>`
});
const SectionTabs: FunctionComponent<IProps> = ({ activeTab }: IProps) => { 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 ( return (
<div <div
data-role='controlgroup' data-role='controlgroup'
data-type='horizontal' data-type='horizontal'
className='localnav' className='localnav'>
dangerouslySetInnerHTML={createLinkElement(activeTab)} <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>
); );
}; };

View file

@ -4,19 +4,9 @@ import { formatDistanceToNow } from 'date-fns';
import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale'; import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale';
import globalize from '../../../lib/globalize'; import globalize from '../../../lib/globalize';
import IconButtonElement from '../../../elements/IconButtonElement'; import IconButtonElement from '../../../elements/IconButtonElement';
import escapeHTML from 'escape-html'; import LinkButton from '../../../elements/emby-button/LinkButton';
import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils'; import { getDefaultBackgroundClass } from '../../cardbuilder/cardBuilderUtils';
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a
is="emby-linkbutton"
class="cardContent"
href="#/dashboard/users/profile?userId=${user.Id}"
>
${renderImgUrl}
</a>`
});
type IProps = { type IProps = {
user?: UserDto; user?: UserDto;
}; };
@ -55,22 +45,21 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
const lastSeen = getLastSeenText(user.LastActivityDate); const lastSeen = getLastSeenText(user.LastActivityDate);
const renderImgUrl = imgUrl ? const renderImgUrl = imgUrl ?
`<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` : <div className={imageClass} style={{ backgroundImage: `url(${imgUrl})` }} /> :
`<div class='${imageClass} ${getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'> <div className={`${imageClass} ${getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center`}>
<span class='material-icons cardImageIcon person' aria-hidden='true'></span> <span className='material-icons cardImageIcon person' aria-hidden='true'></span>
</div>`; </div>;
return ( return (
<div data-userid={user.Id} data-username={user.Name} className={cssClass}> <div data-userid={user.Id} data-username={user.Name} className={cssClass}>
<div className='cardBox visualCardBox'> <div className='cardBox visualCardBox'>
<div className='cardScalable visualCardBox-cardScalable'> <div className='cardScalable visualCardBox-cardScalable'>
<div className='cardPadder cardPadder-square'></div> <div className='cardPadder cardPadder-square'></div>
<div <LinkButton
dangerouslySetInnerHTML={createLinkElement({ className='cardContent'
user: user, href={`#/dashboard/users/profile?userId=${user.Id}`}>
renderImgUrl: renderImgUrl {renderImgUrl}
})} </LinkButton>
/>
</div> </div>
<div className='cardFooter visualCardBox-cardFooter'> <div className='cardFooter visualCardBox-cardFooter'>
<div <div
@ -83,7 +72,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
/> />
</div> </div>
<div className='cardText'> <div className='cardText'>
<span>{escapeHTML(user.Name)}</span> <span>{user.Name}</span>
</div> </div>
<div className='cardText cardText-secondary'> <div className='cardText cardText-secondary'>
<span>{lastSeen != '' ? lastSeen : ''}</span> <span>{lastSeen != '' ? lastSeen : ''}</span>

View file

@ -1,17 +1,19 @@
import React, { type FC } from 'react'; import React, { FunctionComponent } from 'react';
import { useSearchSuggestions } from 'hooks/searchHook';
import Loading from 'components/loading/LoadingComponent'; import Loading from 'components/loading/LoadingComponent';
import { appRouter } from '../router/appRouter'; import { appRouter } from '../router/appRouter';
import globalize from '../../lib/globalize'; import { useSearchSuggestions } from 'hooks/searchHook/useSearchSuggestions';
import LinkButton from 'elements/emby-button/LinkButton'; import globalize from 'lib/globalize';
import LinkButton from '../../elements/emby-button/LinkButton';
import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/emby-button';
interface SearchSuggestionsProps { type SearchSuggestionsProps = {
parentId?: string; parentId?: string | null;
} };
const SearchSuggestions: FC<SearchSuggestionsProps> = ({ parentId }) => { const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }) => {
const { isLoading, data: suggestions } = useSearchSuggestions(parentId); const { isLoading, data: suggestions } = useSearchSuggestions(parentId || undefined);
if (isLoading) return <Loading />; if (isLoading) return <Loading />;
@ -27,15 +29,12 @@ const SearchSuggestions: FC<SearchSuggestionsProps> = ({ parentId }) => {
</div> </div>
<div className='searchSuggestionsList padded-left padded-right'> <div className='searchSuggestionsList padded-left padded-right'>
{suggestions?.map((item) => ( {suggestions?.map(item => (
<div key={`suggestion-${item.Id}`}> <div key={item.Id}>
<LinkButton <LinkButton
className='button-link' className='button-link'
style={{ display: 'inline-block', padding: '0.5em 1em' }}
href={appRouter.getRouteUrl(item)} href={appRouter.getRouteUrl(item)}
style={{
display: 'inline-block',
padding: '0.5em 1em'
}}
> >
{item.Name} {item.Name}
</LinkButton> </LinkButton>

View file

@ -1,6 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import IconButtonElement from './IconButtonElement'; import IconButtonElement from './IconButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement'; import LinkButton from './emby-button/LinkButton';
import globalize from 'lib/globalize';
type IProps = { type IProps = {
SectionClassName?: string; SectionClassName?: string;
@ -28,11 +29,13 @@ const SectionTitleContainer: FunctionComponent<IProps> = ({ SectionClassName, ti
icon={btnIcon} icon={btnIcon}
/>} />}
{isLinkVisible && <SectionTitleLinkElement {isLinkVisible && <LinkButton
className='raised button-alt headerHelpButton' className='raised button-alt headerHelpButton'
title='Help' target='_blank'
url={url} rel='noopener noreferrer'
/>} href={url}>
{globalize.translate('Help')}
</LinkButton>}
</div> </div>
); );

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from 'lib/globalize';
const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
__html: `<a
is="emby-linkbutton"
rel="noopener noreferrer"
class="${className}"
target="_blank"
href="${href}"
>
${title}
</a>`
});
type IProps = {
title?: string;
className?: string;
url?: string
};
const SectionTitleLinkElement: FunctionComponent<IProps> = ({ className, title, url }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createLinkElement({
className: className,
title: globalize.translate(title),
href: url
})}
/>
);
};
export default SectionTitleLinkElement;

View file

@ -20,6 +20,7 @@ const LinkButton: React.FC<LinkButtonProps> = ({
isAutoHideEnabled, isAutoHideEnabled,
href, href,
target, target,
onClick,
children, children,
...rest ...rest
}) => { }) => {
@ -41,7 +42,8 @@ const LinkButton: React.FC<LinkButtonProps> = ({
} else { } else {
e.preventDefault(); e.preventDefault();
} }
}, [ href, target ]); onClick?.(e);
}, [ href, target, onClick ]);
if (isAutoHideEnabled === true && !appHost.supports('externallinks')) { if (isAutoHideEnabled === true && !appHost.supports('externallinks')) {
return null; return null;