Merge pull request #7088 from viown/replace-toast-with-snackbar-on-dashboard

Replace toast with snackbar on dashboard
This commit is contained in:
Bill Thornton
2025-09-30 13:13:03 -04:00
committed by GitHub
8 changed files with 118 additions and 26 deletions

View File

@@ -0,0 +1,30 @@
import React, { useCallback } from 'react';
import Snackbar, { SnackbarProps } from '@mui/material/Snackbar';
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
const Toast = (props: SnackbarProps) => {
const onCloseClick = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
props.onClose?.(e, 'clickaway');
}, [ props ]);
const action = (
<IconButton
size='small'
color='inherit'
onClick={onCloseClick}
>
<CloseIcon fontSize='small' />
</IconButton>
);
return (
<Snackbar
autoHideDuration={3300}
action={action}
{ ...props }
/>
);
};
export default Toast;

View File

@@ -7,7 +7,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Box from '@mui/material/Box';
import globalize from 'lib/globalize';
import React, { FunctionComponent, useCallback } from 'react';
import React, { FunctionComponent, useCallback, useState } from 'react';
import Stack from '@mui/material/Stack';
import FormGroup from '@mui/material/FormGroup';
import FormControl from '@mui/material/FormControl';
@@ -16,7 +16,7 @@ import Checkbox from '@mui/material/Checkbox';
import ContentCopy from '@mui/icons-material/ContentCopy';
import IconButton from '@mui/material/IconButton';
import { copy } from 'scripts/clipboard';
import toast from 'components/toast/toast';
import Toast from 'apps/dashboard/components/Toast';
type IProps = {
backup: BackupManifestDto;
@@ -25,10 +25,16 @@ type IProps = {
};
const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }: IProps) => {
const [ isCopiedToastOpen, setIsCopiedToastOpen ] = useState(false);
const handleToastClose = useCallback(() => {
setIsCopiedToastOpen(false);
}, []);
const copyPath = useCallback(async () => {
if (backup.Path) {
await copy(backup.Path);
toast({ text: globalize.translate('Copied') });
setIsCopiedToastOpen(true);
}
}, [ backup.Path ]);
@@ -39,6 +45,11 @@ const BackupInfoDialog: FunctionComponent<IProps> = ({ backup, open, onClose }:
maxWidth={'sm'}
fullWidth
>
<Toast
open={isCopiedToastOpen}
onClose={handleToastClose}
message={globalize.translate('Copied')}
/>
<DialogTitle>
{backup.DateCreated}
</DialogTitle>

View File

@@ -1,6 +1,6 @@
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useServerLog } from 'apps/dashboard/features/logs/api/useServerLog';
import Alert from '@mui/material/Alert';
@@ -13,8 +13,8 @@ import Typography from '@mui/material/Typography';
import ContentCopy from '@mui/icons-material/ContentCopy';
import FileDownload from '@mui/icons-material/FileDownload';
import globalize from 'lib/globalize';
import toast from 'components/toast/toast';
import { copy } from 'scripts/clipboard';
import Toast from 'apps/dashboard/components/Toast';
export const Component = () => {
const { file: fileName } = useParams();
@@ -24,13 +24,18 @@ export const Component = () => {
data: log,
refetch
} = useServerLog(fileName ?? '');
const [ isCopiedToastOpen, setIsCopiedToastOpen ] = useState(false);
const retry = useCallback(() => refetch(), [refetch]);
const handleToastClose = useCallback(() => {
setIsCopiedToastOpen(false);
}, []);
const copyToClipboard = useCallback(async () => {
if (log) {
await copy(log);
toast({ text: globalize.translate('CopyLogSuccess') });
setIsCopiedToastOpen(true);
}
}, [log]);
@@ -52,6 +57,11 @@ export const Component = () => {
title={fileName}
className='mainAnimatedPage type-interior'
>
<Toast
open={isCopiedToastOpen}
onClose={handleToastClose}
message={globalize.translate('CopyLogSuccess')}
/>
<Container className='content-primary'>
<Box>
<Typography variant='h1'>{fileName}</Typography>

View File

@@ -4,13 +4,13 @@ import { useSearchParams } from 'react-router-dom';
import loading from '../../../../components/loading/loading';
import globalize from '../../../../lib/globalize';
import toast from '../../../../components/toast/toast';
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 Toast from 'apps/dashboard/components/Toast';
type ItemsArr = {
Name?: string | null;
@@ -23,6 +23,7 @@ type ItemsArr = {
const UserLibraryAccess = () => {
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
@@ -31,6 +32,10 @@ const UserLibraryAccess = () => {
const element = useRef<HTMLDivElement>(null);
const handleToastClose = useCallback(() => {
setIsSettingsSavedToastOpen(false);
}, []);
const triggerChange = (select: HTMLInputElement) => {
const evt = new Event('change', { bubbles: false, cancelable: true });
select.dispatchEvent(evt);
@@ -220,7 +225,7 @@ const UserLibraryAccess = () => {
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
setIsSettingsSavedToastOpen(true);
};
(page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
@@ -243,6 +248,11 @@ const UserLibraryAccess = () => {
id='userLibraryAccessPage'
className='mainAnimatedPage type-interior'
>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -4,13 +4,13 @@ import React, { useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import Input from '../../../../elements/emby-input/Input';
import Button from '../../../../elements/emby-button/Button';
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import Page from '../../../../components/Page';
import Toast from 'apps/dashboard/components/Toast';
type UserInput = {
Name?: string;
@@ -25,8 +25,13 @@ type ItemsArr = {
const UserNew = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const [ isErrorToastOpen, setIsErrorToastOpen ] = useState(false);
const element = useRef<HTMLDivElement>(null);
const handleToastClose = useCallback(() => {
setIsErrorToastOpen(false);
}, []);
const getItemsResult = (items: BaseItemDto[]) => {
return items.map(item =>
({
@@ -150,7 +155,7 @@ const UserNew = () => {
console.error('[usernew] failed to update user policy', err);
});
}, function () {
toast(globalize.translate('ErrorDefault'));
setIsErrorToastOpen(true);
loading.hide();
});
};
@@ -185,6 +190,11 @@ const UserNew = () => {
id='newUserPage'
className='mainAnimatedPage type-interior'
>
<Toast
open={isErrorToastOpen}
onClose={handleToastClose}
message={globalize.translate('ErrorDefault')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -1,5 +1,5 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
@@ -14,6 +14,8 @@ import '../../../../components/cardbuilder/card.scss';
import '../../../../components/indicators/indicators.scss';
import '../../../../styles/flexstyles.scss';
import Page from '../../../../components/Page';
import { useLocation } from 'react-router-dom';
import Toast from 'apps/dashboard/components/Toast';
type MenuEntry = {
name?: string;
@@ -22,10 +24,16 @@ type MenuEntry = {
};
const UserProfiles = () => {
const location = useLocation();
const [ isSettingsSavedToastOpen, setIsSettingsSavedToastOpen ] = useState(false);
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null);
const handleToastClose = useCallback(() => {
setIsSettingsSavedToastOpen(false);
}, []);
const loadData = () => {
loading.show();
window.ApiClient.getUsers().then(function (result) {
@@ -39,6 +47,11 @@ const UserProfiles = () => {
useEffect(() => {
const page = element.current;
if (location.state?.openSavedToast) {
setIsSettingsSavedToastOpen(true);
window.history.replaceState({}, '');
}
if (!page) {
console.error('Unexpected null reference');
return;
@@ -161,6 +174,11 @@ const UserProfiles = () => {
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
title={globalize.translate('HeaderUsers')}
>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -12,12 +12,12 @@ 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 toast from '../../../../components/toast/toast';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
import prompt from '../../../../components/prompt/prompt';
import { ServerConnections } from 'lib/jellyfin-apiclient';
import Toast from 'apps/dashboard/components/Toast';
type NamedItem = {
name: string;
@@ -74,11 +74,16 @@ const UserParentalControl = () => {
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
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 element = useRef<HTMLDivElement>(null);
const parentalRatingsRef = useRef<ParentalRating[]>([]);
const handleToastClose = useCallback(() => {
setIsSettingsSavedToastOpen(false);
}, []);
const loadUnratedItems = useCallback((user: UserDto) => {
const page = element.current;
@@ -300,7 +305,7 @@ const UserParentalControl = () => {
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
setIsSettingsSavedToastOpen(true);
};
const saveUser = handleSaveUser(page, parentalRatingsRef, getSchedulesFromPage, getAllowedTagsFromPage, getBlockedTagsFromPage, onSaveComplete);
@@ -387,6 +392,11 @@ const UserParentalControl = () => {
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<Toast
open={isSettingsSavedToastOpen}
onClose={handleToastClose}
message={globalize.translate('SettingsSaved')}
/>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer

View File

@@ -1,9 +1,8 @@
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 { useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import Dashboard from '../../../../utils/dashboard';
import globalize from '../../../../lib/globalize';
import Button from '../../../../elements/emby-button/Button';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
@@ -12,7 +11,6 @@ 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 toast from '../../../../components/toast/toast';
import SelectElement from '../../../../elements/SelectElement';
import Page from '../../../../components/Page';
@@ -25,16 +23,8 @@ const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
.map(e => e.getAttribute('data-id'))
);
function onSaveComplete() {
Dashboard.navigate('/dashboard/users')
.catch(err => {
console.error('[useredit] failed to navigate to user profile', err);
});
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const UserEdit = () => {
const navigate = useNavigate();
const [ searchParams ] = useSearchParams();
const userId = searchParams.get('userId');
const [ userDto, setUserDto ] = useState<UserDto>();
@@ -228,7 +218,10 @@ const UserEdit = () => {
window.ApiClient.updateUser(user).then(() => (
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || { PasswordResetProviderId: '', AuthenticationProviderId: '' })
)).then(() => {
onSaveComplete();
navigate('/dashboard/users', {
state: { openSavedToast: true }
});
loading.hide();
}).catch(err => {
console.error('[useredit] failed to update user', err);
});