Enhancement: Loading feedback for all confirmation dialogs: Extended visual loading indicators across all confirmation dialogs throughout the application. Delete, cleanup, and bulk operation dialogs now show an animated dots loader and disabled state during async operations, providing consistent user feedback for backups (restore/delete), channels, EPGs, logos, VOD logos, M3U accounts, streams, users, groups, filters, profiles, batch operations, and network access changes.
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run

This commit is contained in:
SergeantPanda 2026-01-12 13:53:44 -06:00
parent 43636a84d0
commit 2a3d0db670
15 changed files with 139 additions and 48 deletions

View file

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Backup restore loading feedback: Added visual loading indicator to the backup restore confirmation dialog. When clicking "Restore", the button now displays an animated dots loader and becomes disabled, providing clear feedback that the restore operation is in progress. - Loading feedback for all confirmation dialogs: Extended visual loading indicators across all confirmation dialogs throughout the application. Delete, cleanup, and bulk operation dialogs now show an animated dots loader and disabled state during async operations, providing consistent user feedback for backups (restore/delete), channels, EPGs, logos, VOD logos, M3U accounts, streams, users, groups, filters, profiles, batch operations, and network access changes.
- Channel profile edit and duplicate functionality: Users can now rename existing channel profiles and create duplicates with automatic channel membership cloning. Each profile action (edit, duplicate, delete) in the profile dropdown for quick access. - Channel profile edit and duplicate functionality: Users can now rename existing channel profiles and create duplicates with automatic channel membership cloning. Each profile action (edit, duplicate, delete) in the profile dropdown for quick access.
- ProfileModal component extracted for improved code organization and maintainability of channel profile management operations. - ProfileModal component extracted for improved code organization and maintainability of channel profile management operations.
- Frontend unit tests for pages and utilities: Added comprehensive unit test coverage for frontend components within pages/ and JS files within utils/, along with a GitHub Actions workflow (`frontend-tests.yml`) to automatically run tests on commits and pull requests - Thanks [@nick4810](https://github.com/nick4810) - Frontend unit tests for pages and utilities: Added comprehensive unit test coverage for frontend components within pages/ and JS files within utils/, along with a GitHub Actions workflow (`frontend-tests.yml`) to automatically run tests on commits and pull requests - Thanks [@nick4810](https://github.com/nick4810)

View file

@ -231,6 +231,7 @@ export default function BackupManager() {
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [selectedBackup, setSelectedBackup] = useState(null); const [selectedBackup, setSelectedBackup] = useState(null);
const [restoring, setRestoring] = useState(false); const [restoring, setRestoring] = useState(false);
const [deleting, setDeleting] = useState(false);
// Read user's preferences from settings // Read user's preferences from settings
const [timeFormat] = useLocalStorage('time-format', '12h'); const [timeFormat] = useLocalStorage('time-format', '12h');
@ -513,6 +514,7 @@ export default function BackupManager() {
}; };
const handleDeleteConfirm = async () => { const handleDeleteConfirm = async () => {
setDeleting(true);
try { try {
await API.deleteBackup(selectedBackup.name); await API.deleteBackup(selectedBackup.name);
notifications.show({ notifications.show({
@ -528,6 +530,7 @@ export default function BackupManager() {
color: 'red', color: 'red',
}); });
} finally { } finally {
setDeleting(false);
setDeleteConfirmOpen(false); setDeleteConfirmOpen(false);
setSelectedBackup(null); setSelectedBackup(null);
} }
@ -968,6 +971,7 @@ export default function BackupManager() {
cancelLabel="Cancel" cancelLabel="Cancel"
actionKey="delete-backup" actionKey="delete-backup"
onSuppressChange={suppressWarning} onSuppressChange={suppressWarning}
loading={deleting}
/> />
</Stack> </Stack>
); );

View file

@ -77,6 +77,9 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
const [confirmSetLogosOpen, setConfirmSetLogosOpen] = useState(false); const [confirmSetLogosOpen, setConfirmSetLogosOpen] = useState(false);
const [confirmSetTvgIdsOpen, setConfirmSetTvgIdsOpen] = useState(false); const [confirmSetTvgIdsOpen, setConfirmSetTvgIdsOpen] = useState(false);
const [confirmBatchUpdateOpen, setConfirmBatchUpdateOpen] = useState(false); const [confirmBatchUpdateOpen, setConfirmBatchUpdateOpen] = useState(false);
const [settingNames, setSettingNames] = useState(false);
const [settingLogos, setSettingLogos] = useState(false);
const [settingTvgIds, setSettingTvgIds] = useState(false);
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
const suppressWarning = useWarningsStore((s) => s.suppressWarning); const suppressWarning = useWarningsStore((s) => s.suppressWarning);
@ -328,6 +331,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
}; };
const executeSetNamesFromEpg = async () => { const executeSetNamesFromEpg = async () => {
setSettingNames(true);
try { try {
// Start the backend task // Start the backend task
await API.setChannelNamesFromEpg(channelIds); await API.setChannelNamesFromEpg(channelIds);
@ -341,7 +345,6 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
}); });
// Close the modal since the task is now running in background // Close the modal since the task is now running in background
setConfirmSetNamesOpen(false);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Failed to start EPG name setting task:', error); console.error('Failed to start EPG name setting task:', error);
@ -350,6 +353,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
message: 'Failed to start EPG name setting task.', message: 'Failed to start EPG name setting task.',
color: 'red', color: 'red',
}); });
} finally {
setSettingNames(false);
setConfirmSetNamesOpen(false); setConfirmSetNamesOpen(false);
} }
}; };
@ -373,6 +378,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
}; };
const executeSetLogosFromEpg = async () => { const executeSetLogosFromEpg = async () => {
setSettingLogos(true);
try { try {
// Start the backend task // Start the backend task
await API.setChannelLogosFromEpg(channelIds); await API.setChannelLogosFromEpg(channelIds);
@ -386,7 +392,6 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
}); });
// Close the modal since the task is now running in background // Close the modal since the task is now running in background
setConfirmSetLogosOpen(false);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Failed to start EPG logo setting task:', error); console.error('Failed to start EPG logo setting task:', error);
@ -395,6 +400,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
message: 'Failed to start EPG logo setting task.', message: 'Failed to start EPG logo setting task.',
color: 'red', color: 'red',
}); });
} finally {
setSettingLogos(false);
setConfirmSetLogosOpen(false); setConfirmSetLogosOpen(false);
} }
}; };
@ -418,6 +425,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
}; };
const executeSetTvgIdsFromEpg = async () => { const executeSetTvgIdsFromEpg = async () => {
setSettingTvgIds(true);
try { try {
// Start the backend task // Start the backend task
await API.setChannelTvgIdsFromEpg(channelIds); await API.setChannelTvgIdsFromEpg(channelIds);
@ -431,7 +439,6 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
}); });
// Close the modal since the task is now running in background // Close the modal since the task is now running in background
setConfirmSetTvgIdsOpen(false);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Failed to start EPG TVG-ID setting task:', error); console.error('Failed to start EPG TVG-ID setting task:', error);
@ -440,6 +447,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
message: 'Failed to start EPG TVG-ID setting task.', message: 'Failed to start EPG TVG-ID setting task.',
color: 'red', color: 'red',
}); });
} finally {
setSettingTvgIds(false);
setConfirmSetTvgIdsOpen(false); setConfirmSetTvgIdsOpen(false);
} }
}; };
@ -947,6 +956,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
opened={confirmSetNamesOpen} opened={confirmSetNamesOpen}
onClose={() => setConfirmSetNamesOpen(false)} onClose={() => setConfirmSetNamesOpen(false)}
onConfirm={executeSetNamesFromEpg} onConfirm={executeSetNamesFromEpg}
loading={settingNames}
title="Confirm Set Names from EPG" title="Confirm Set Names from EPG"
message={ message={
<div style={{ whiteSpace: 'pre-line' }}> <div style={{ whiteSpace: 'pre-line' }}>
@ -968,6 +978,7 @@ This action cannot be undone.`}
opened={confirmSetLogosOpen} opened={confirmSetLogosOpen}
onClose={() => setConfirmSetLogosOpen(false)} onClose={() => setConfirmSetLogosOpen(false)}
onConfirm={executeSetLogosFromEpg} onConfirm={executeSetLogosFromEpg}
loading={settingLogos}
title="Confirm Set Logos from EPG" title="Confirm Set Logos from EPG"
message={ message={
<div style={{ whiteSpace: 'pre-line' }}> <div style={{ whiteSpace: 'pre-line' }}>
@ -989,6 +1000,7 @@ This action cannot be undone.`}
opened={confirmSetTvgIdsOpen} opened={confirmSetTvgIdsOpen}
onClose={() => setConfirmSetTvgIdsOpen(false)} onClose={() => setConfirmSetTvgIdsOpen(false)}
onConfirm={executeSetTvgIdsFromEpg} onConfirm={executeSetTvgIdsFromEpg}
loading={settingTvgIds}
title="Confirm Set TVG-IDs from EPG" title="Confirm Set TVG-IDs from EPG"
message={ message={
<div style={{ whiteSpace: 'pre-line' }}> <div style={{ whiteSpace: 'pre-line' }}>
@ -1010,6 +1022,7 @@ This action cannot be undone.`}
opened={confirmBatchUpdateOpen} opened={confirmBatchUpdateOpen}
onClose={() => setConfirmBatchUpdateOpen(false)} onClose={() => setConfirmBatchUpdateOpen(false)}
onConfirm={onSubmit} onConfirm={onSubmit}
loading={isSubmitting}
title="Confirm Batch Update" title="Confirm Batch Update"
message={ message={
<div> <div>

View file

@ -183,6 +183,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [groupToDelete, setGroupToDelete] = useState(null); const [groupToDelete, setGroupToDelete] = useState(null);
const [confirmCleanupOpen, setConfirmCleanupOpen] = useState(false); const [confirmCleanupOpen, setConfirmCleanupOpen] = useState(false);
const [deletingGroup, setDeletingGroup] = useState(false);
// Memoize the channel groups array to prevent unnecessary re-renders // Memoize the channel groups array to prevent unnecessary re-renders
const channelGroupsArray = useMemo( const channelGroupsArray = useMemo(
@ -382,6 +383,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
const executeDeleteGroup = useCallback( const executeDeleteGroup = useCallback(
async (group) => { async (group) => {
setDeletingGroup(true);
try { try {
await API.deleteChannelGroup(group.id); await API.deleteChannelGroup(group.id);
@ -392,13 +394,14 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
}); });
await fetchGroupUsage(); // Refresh usage data await fetchGroupUsage(); // Refresh usage data
setConfirmDeleteOpen(false);
} catch (error) { } catch (error) {
notifications.show({ notifications.show({
title: 'Error', title: 'Error',
message: 'Failed to delete group', message: 'Failed to delete group',
color: 'red', color: 'red',
}); });
} finally {
setDeletingGroup(false);
setConfirmDeleteOpen(false); setConfirmDeleteOpen(false);
} }
}, },
@ -680,6 +683,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => groupToDelete && executeDeleteGroup(groupToDelete)} onConfirm={() => groupToDelete && executeDeleteGroup(groupToDelete)}
loading={deletingGroup}
title="Confirm Group Deletion" title="Confirm Group Deletion"
message={ message={
groupToDelete ? ( groupToDelete ? (
@ -706,6 +710,7 @@ This action cannot be undone.`}
opened={confirmCleanupOpen} opened={confirmCleanupOpen}
onClose={() => setConfirmCleanupOpen(false)} onClose={() => setConfirmCleanupOpen(false)}
onConfirm={executeCleanup} onConfirm={executeCleanup}
loading={isCleaningUp}
title="Confirm Group Cleanup" title="Confirm Group Cleanup"
message={ message={
<div style={{ whiteSpace: 'pre-line' }}> <div style={{ whiteSpace: 'pre-line' }}>

View file

@ -151,6 +151,7 @@ const M3UFilters = ({ playlist, isOpen, onClose }) => {
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [filterToDelete, setFilterToDelete] = useState(null); const [filterToDelete, setFilterToDelete] = useState(null);
const [filters, setFilters] = useState([]); const [filters, setFilters] = useState([]);
const [deleting, setDeleting] = useState(false);
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
const suppressWarning = useWarningsStore((s) => s.suppressWarning); const suppressWarning = useWarningsStore((s) => s.suppressWarning);
@ -192,16 +193,17 @@ const M3UFilters = ({ playlist, isOpen, onClose }) => {
const deleteFilter = async (id) => { const deleteFilter = async (id) => {
if (!playlist || !playlist.id) return; if (!playlist || !playlist.id) return;
setDeleting(true);
try { try {
await API.deleteM3UFilter(playlist.id, id); await API.deleteM3UFilter(playlist.id, id);
setConfirmDeleteOpen(false); fetchPlaylist(playlist.id);
setFilters(filters.filter((f) => f.id !== id));
} catch (error) { } catch (error) {
console.error('Error deleting profile:', error); console.error('Error deleting profile:', error);
} finally {
setDeleting(false);
setConfirmDeleteOpen(false); setConfirmDeleteOpen(false);
} }
fetchPlaylist(playlist.id);
setFilters(filters.filter((f) => f.id !== id));
}; };
const closeEditor = (updatedPlaylist = null) => { const closeEditor = (updatedPlaylist = null) => {
@ -321,6 +323,7 @@ const M3UFilters = ({ playlist, isOpen, onClose }) => {
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => deleteFilter(deleteTarget)} onConfirm={() => deleteFilter(deleteTarget)}
loading={deleting}
title="Confirm Filter Deletion" title="Confirm Filter Deletion"
message={ message={
filterToDelete ? ( filterToDelete ? (

View file

@ -38,6 +38,7 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [profileToDelete, setProfileToDelete] = useState(null); const [profileToDelete, setProfileToDelete] = useState(null);
const [deletingProfile, setDeletingProfile] = useState(false);
const [accountInfoOpen, setAccountInfoOpen] = useState(false); const [accountInfoOpen, setAccountInfoOpen] = useState(false);
const [selectedProfileForInfo, setSelectedProfileForInfo] = useState(null); const [selectedProfileForInfo, setSelectedProfileForInfo] = useState(null);
@ -88,11 +89,13 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
const executeDeleteProfile = async (id) => { const executeDeleteProfile = async (id) => {
if (!playlist || !playlist.id) return; if (!playlist || !playlist.id) return;
setDeletingProfile(true);
try { try {
await API.deleteM3UProfile(playlist.id, id); await API.deleteM3UProfile(playlist.id, id);
setConfirmDeleteOpen(false);
} catch (error) { } catch (error) {
console.error('Error deleting profile:', error); console.error('Error deleting profile:', error);
} finally {
setDeletingProfile(false);
setConfirmDeleteOpen(false); setConfirmDeleteOpen(false);
} }
}; };
@ -359,6 +362,7 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeleteProfile(deleteTarget)} onConfirm={() => executeDeleteProfile(deleteTarget)}
loading={deletingProfile}
title="Confirm Profile Deletion" title="Confirm Profile Deletion"
message={ message={
profileToDelete ? ( profileToDelete ? (

View file

@ -20,6 +20,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
const [networkAccessConfirmOpen, setNetworkAccessConfirmOpen] = const [networkAccessConfirmOpen, setNetworkAccessConfirmOpen] =
useState(false); useState(false);
const [saving, setSaving] = useState(false);
const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] = const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] =
useState([]); useState([]);
const [clientIpAddress, setClientIpAddress] = useState(null); const [clientIpAddress, setClientIpAddress] = useState(null);
@ -31,7 +32,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
}); });
useEffect(() => { useEffect(() => {
if(!active) setSaved(false); if (!active) setSaved(false);
}, [active]); }, [active]);
useEffect(() => { useEffect(() => {
@ -74,19 +75,22 @@ const NetworkAccessForm = React.memo(({ active }) => {
const saveNetworkAccess = async () => { const saveNetworkAccess = async () => {
setSaved(false); setSaved(false);
setSaving(true);
try { try {
await updateSetting({ await updateSetting({
...settings['network-access'], ...settings['network-access'],
value: JSON.stringify(networkAccessForm.getValues()), value: JSON.stringify(networkAccessForm.getValues()),
}); });
setSaved(true); setSaved(true);
setNetworkAccessConfirmOpen(false);
} catch (e) { } catch (e) {
const errors = {}; const errors = {};
for (const key in e.body.value) { for (const key in e.body.value) {
errors[key] = `Invalid CIDR(s): ${e.body.value[key]}`; errors[key] = `Invalid CIDR(s): ${e.body.value[key]}`;
} }
networkAccessForm.setErrors(errors); networkAccessForm.setErrors(errors);
} finally {
setSaving(false);
setNetworkAccessConfirmOpen(false);
} }
}; };
@ -135,6 +139,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
onClose={() => setNetworkAccessConfirmOpen(false)} onClose={() => setNetworkAccessConfirmOpen(false)}
onConfirm={saveNetworkAccess} onConfirm={saveNetworkAccess}
title={`Confirm Network Access Blocks`} title={`Confirm Network Access Blocks`}
loading={saving}
message={ message={
<> <>
<Text> <Text>
@ -158,4 +163,4 @@ const NetworkAccessForm = React.memo(({ active }) => {
); );
}); });
export default NetworkAccessForm; export default NetworkAccessForm;

View file

@ -316,6 +316,7 @@ const ChannelsTable = ({ onReady }) => {
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [isBulkDelete, setIsBulkDelete] = useState(false); const [isBulkDelete, setIsBulkDelete] = useState(false);
const [channelToDelete, setChannelToDelete] = useState(null); const [channelToDelete, setChannelToDelete] = useState(null);
const [deleting, setDeleting] = useState(false);
const hasFetchedData = useRef(false); const hasFetchedData = useRef(false);
@ -545,9 +546,14 @@ const ChannelsTable = ({ onReady }) => {
}; };
const executeDeleteChannel = async (id) => { const executeDeleteChannel = async (id) => {
await API.deleteChannel(id); setDeleting(true);
API.requeryChannels(); try {
setConfirmDeleteOpen(false); await API.deleteChannel(id);
API.requeryChannels();
} finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
}; };
const deleteChannels = async () => { const deleteChannels = async () => {
@ -562,12 +568,17 @@ const ChannelsTable = ({ onReady }) => {
const executeDeleteChannels = async () => { const executeDeleteChannels = async () => {
setIsLoading(true); setIsLoading(true);
await API.deleteChannels(table.selectedTableIds); setDeleting(true);
await API.requeryChannels(); try {
setSelectedChannelIds([]); await API.deleteChannels(table.selectedTableIds);
table.setSelectedTableIds([]); await API.requeryChannels();
setIsLoading(false); setSelectedChannelIds([]);
setConfirmDeleteOpen(false); table.setSelectedTableIds([]);
} finally {
setDeleting(false);
setIsLoading(false);
setConfirmDeleteOpen(false);
}
}; };
const createRecording = (channel) => { const createRecording = (channel) => {
@ -1498,6 +1509,7 @@ const ChannelsTable = ({ onReady }) => {
? executeDeleteChannels() ? executeDeleteChannels()
: executeDeleteChannel(deleteTarget) : executeDeleteChannel(deleteTarget)
} }
loading={deleting}
title={`Confirm ${isBulkDelete ? 'Bulk ' : ''}Channel Deletion`} title={`Confirm ${isBulkDelete ? 'Bulk ' : ''}Channel Deletion`}
message={ message={
isBulkDelete ? ( isBulkDelete ? (

View file

@ -118,6 +118,7 @@ const ChannelTableHeader = ({
const [confirmDeleteProfileOpen, setConfirmDeleteProfileOpen] = const [confirmDeleteProfileOpen, setConfirmDeleteProfileOpen] =
useState(false); useState(false);
const [profileToDelete, setProfileToDelete] = useState(null); const [profileToDelete, setProfileToDelete] = useState(null);
const [deletingProfile, setDeletingProfile] = useState(false);
const [profileModalState, setProfileModalState] = useState({ const [profileModalState, setProfileModalState] = useState({
opened: false, opened: false,
mode: null, mode: null,
@ -157,8 +158,13 @@ const ChannelTableHeader = ({
}; };
const executeDeleteProfile = async (id) => { const executeDeleteProfile = async (id) => {
await API.deleteChannelProfile(id); setDeletingProfile(true);
setConfirmDeleteProfileOpen(false); try {
await API.deleteChannelProfile(id);
} finally {
setDeletingProfile(false);
setConfirmDeleteProfileOpen(false);
}
}; };
const matchEpg = async () => { const matchEpg = async () => {
@ -402,6 +408,7 @@ const ChannelTableHeader = ({
opened={confirmDeleteProfileOpen} opened={confirmDeleteProfileOpen}
onClose={() => setConfirmDeleteProfileOpen(false)} onClose={() => setConfirmDeleteProfileOpen(false)}
onConfirm={() => executeDeleteProfile(profileToDelete?.id)} onConfirm={() => executeDeleteProfile(profileToDelete?.id)}
loading={deletingProfile}
title="Confirm Profile Deletion" title="Confirm Profile Deletion"
message={ message={
profileToDelete ? ( profileToDelete ? (

View file

@ -110,6 +110,7 @@ const EPGsTable = () => {
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [epgToDelete, setEpgToDelete] = useState(null); const [epgToDelete, setEpgToDelete] = useState(null);
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [deleting, setDeleting] = useState(false);
const epgs = useEPGsStore((s) => s.epgs); const epgs = useEPGsStore((s) => s.epgs);
const refreshProgress = useEPGsStore((s) => s.refreshProgress); const refreshProgress = useEPGsStore((s) => s.refreshProgress);
@ -431,10 +432,13 @@ const EPGsTable = () => {
}; };
const executeDeleteEPG = async (id) => { const executeDeleteEPG = async (id) => {
setIsLoading(true); setDeleting(true);
await API.deleteEPG(id); try {
setIsLoading(false); await API.deleteEPG(id);
setConfirmDeleteOpen(false); } finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
}; };
const refreshEPG = async (id) => { const refreshEPG = async (id) => {
@ -688,6 +692,7 @@ const EPGsTable = () => {
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeleteEPG(deleteTarget)} onConfirm={() => executeDeleteEPG(deleteTarget)}
loading={deleting}
title="Confirm EPG Source Deletion" title="Confirm EPG Source Deletion"
message={ message={
epgToDelete ? ( epgToDelete ? (

View file

@ -189,12 +189,12 @@ const LogosTable = () => {
color: 'red', color: 'red',
}); });
} finally { } finally {
setIsLoading(false);
setConfirmDeleteOpen(false); setConfirmDeleteOpen(false);
setDeleteTarget(null); setDeleteTarget(null);
setLogoToDelete(null); setLogoToDelete(null);
setIsBulkDelete(false); setIsBulkDelete(false);
clearSelections(); // Clear selections clearSelections(); // Clear selections
setIsLoading(false);
} }
}, },
[fetchAllLogos, clearSelections] [fetchAllLogos, clearSelections]
@ -221,10 +221,10 @@ const LogosTable = () => {
color: 'red', color: 'red',
}); });
} finally { } finally {
setIsLoading(false);
setConfirmDeleteOpen(false); setConfirmDeleteOpen(false);
setIsBulkDelete(false); setIsBulkDelete(false);
clearSelections(); // Clear selections clearSelections(); // Clear selections
setIsLoading(false);
} }
}, },
[selectedRows, fetchAllLogos, clearSelections] [selectedRows, fetchAllLogos, clearSelections]
@ -805,6 +805,7 @@ const LogosTable = () => {
<ConfirmationDialog <ConfirmationDialog
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
loading={isLoading}
onConfirm={(deleteFiles) => { onConfirm={(deleteFiles) => {
if (isBulkDelete) { if (isBulkDelete) {
executeBulkDelete(deleteFiles); executeBulkDelete(deleteFiles);
@ -867,6 +868,7 @@ const LogosTable = () => {
<ConfirmationDialog <ConfirmationDialog
opened={confirmCleanupOpen} opened={confirmCleanupOpen}
onClose={() => setConfirmCleanupOpen(false)} onClose={() => setConfirmCleanupOpen(false)}
loading={isCleaningUp}
onConfirm={executeCleanupUnused} onConfirm={executeCleanupUnused}
title="Cleanup Unused Logos" title="Cleanup Unused Logos"
message={ message={

View file

@ -140,6 +140,7 @@ const M3UTable = () => {
const [playlistToDelete, setPlaylistToDelete] = useState(null); const [playlistToDelete, setPlaylistToDelete] = useState(null);
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [sorting, setSorting] = useState([{ id: 'name', desc: '' }]); const [sorting, setSorting] = useState([{ id: 'name', desc: '' }]);
const [deleting, setDeleting] = useState(false);
const playlists = usePlaylistsStore((s) => s.playlists); const playlists = usePlaylistsStore((s) => s.playlists);
const refreshProgress = usePlaylistsStore((s) => s.refreshProgress); const refreshProgress = usePlaylistsStore((s) => s.refreshProgress);
@ -400,9 +401,14 @@ const M3UTable = () => {
const executeDeletePlaylist = async (id) => { const executeDeletePlaylist = async (id) => {
setIsLoading(true); setIsLoading(true);
await API.deletePlaylist(id); setDeleting(true);
setIsLoading(false); try {
setConfirmDeleteOpen(false); await API.deletePlaylist(id);
} finally {
setDeleting(false);
setIsLoading(false);
setConfirmDeleteOpen(false);
}
}; };
const toggleActive = async (playlist) => { const toggleActive = async (playlist) => {
@ -893,6 +899,7 @@ const M3UTable = () => {
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeletePlaylist(deleteTarget)} onConfirm={() => executeDeletePlaylist(deleteTarget)}
loading={deleting}
title="Confirm M3U Account Deletion" title="Confirm M3U Account Deletion"
message={ message={
playlistToDelete ? ( playlistToDelete ? (

View file

@ -215,6 +215,7 @@ const StreamsTable = ({ onReady }) => {
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [streamToDelete, setStreamToDelete] = useState(null); const [streamToDelete, setStreamToDelete] = useState(null);
const [isBulkDelete, setIsBulkDelete] = useState(false); const [isBulkDelete, setIsBulkDelete] = useState(false);
const [deleting, setDeleting] = useState(false);
// const [allRowsSelected, setAllRowsSelected] = useState(false); // const [allRowsSelected, setAllRowsSelected] = useState(false);
@ -590,12 +591,17 @@ const StreamsTable = ({ onReady }) => {
}; };
const executeDeleteStream = async (id) => { const executeDeleteStream = async (id) => {
await API.deleteStream(id); setDeleting(true);
fetchData(); try {
// Clear the selection for the deleted stream await API.deleteStream(id);
setSelectedStreamIds([]); fetchData();
table.setSelectedTableIds([]); // Clear the selection for the deleted stream
setConfirmDeleteOpen(false); setSelectedStreamIds([]);
table.setSelectedTableIds([]);
} finally {
setDeleting(false);
setConfirmDeleteOpen(false);
}
}; };
const deleteStreams = async () => { const deleteStreams = async () => {
@ -612,12 +618,17 @@ const StreamsTable = ({ onReady }) => {
const executeDeleteStreams = async () => { const executeDeleteStreams = async () => {
setIsLoading(true); setIsLoading(true);
await API.deleteStreams(selectedStreamIds); setDeleting(true);
setIsLoading(false); try {
fetchData(); await API.deleteStreams(selectedStreamIds);
setSelectedStreamIds([]); fetchData();
table.setSelectedTableIds([]); setSelectedStreamIds([]);
setConfirmDeleteOpen(false); table.setSelectedTableIds([]);
} finally {
setDeleting(false);
setIsLoading(false);
setConfirmDeleteOpen(false);
}
}; };
const closeStreamForm = () => { const closeStreamForm = () => {
@ -1258,6 +1269,7 @@ This action cannot be undone.`}
cancelLabel="Cancel" cancelLabel="Cancel"
actionKey={isBulkDelete ? 'delete-streams' : 'delete-stream'} actionKey={isBulkDelete ? 'delete-streams' : 'delete-stream'}
onSuppressChange={suppressWarning} onSuppressChange={suppressWarning}
loading={deleting}
size="md" size="md"
/> />
</> </>

View file

@ -96,6 +96,7 @@ const UsersTable = () => {
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [userToDelete, setUserToDelete] = useState(null); const [userToDelete, setUserToDelete] = useState(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [deleting, setDeleting] = useState(false);
const [visiblePasswords, setVisiblePasswords] = useState({}); const [visiblePasswords, setVisiblePasswords] = useState({});
/** /**
@ -110,9 +111,14 @@ const UsersTable = () => {
const executeDeleteUser = useCallback(async (id) => { const executeDeleteUser = useCallback(async (id) => {
setIsLoading(true); setIsLoading(true);
await API.deleteUser(id); setDeleting(true);
setIsLoading(false); try {
setConfirmDeleteOpen(false); await API.deleteUser(id);
} finally {
setDeleting(false);
setIsLoading(false);
setConfirmDeleteOpen(false);
}
}, []); }, []);
const editUser = useCallback(async (user = null) => { const editUser = useCallback(async (user = null) => {
@ -406,6 +412,7 @@ const UsersTable = () => {
opened={confirmDeleteOpen} opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)} onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeleteUser(deleteTarget)} onConfirm={() => executeDeleteUser(deleteTarget)}
loading={deleting}
title="Confirm User Deletion" title="Confirm User Deletion"
message={ message={
userToDelete ? ( userToDelete ? (

View file

@ -74,6 +74,7 @@ export default function VODLogosTable() {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null);
const [confirmCleanupOpen, setConfirmCleanupOpen] = useState(false); const [confirmCleanupOpen, setConfirmCleanupOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const [paginationString, setPaginationString] = useState(''); const [paginationString, setPaginationString] = useState('');
const [isCleaningUp, setIsCleaningUp] = useState(false); const [isCleaningUp, setIsCleaningUp] = useState(false);
const tableRef = React.useRef(null); const tableRef = React.useRef(null);
@ -139,6 +140,7 @@ export default function VODLogosTable() {
}, []); }, []);
const handleConfirmDelete = async () => { const handleConfirmDelete = async () => {
setDeleting(true);
try { try {
if (deleteTarget.length === 1) { if (deleteTarget.length === 1) {
await deleteVODLogo(deleteTarget[0]); await deleteVODLogo(deleteTarget[0]);
@ -162,6 +164,7 @@ export default function VODLogosTable() {
color: 'red', color: 'red',
}); });
} finally { } finally {
setDeleting(false);
// Always clear selections and close dialog, even on error // Always clear selections and close dialog, even on error
clearSelections(); clearSelections();
setConfirmDeleteOpen(false); setConfirmDeleteOpen(false);
@ -571,6 +574,7 @@ export default function VODLogosTable() {
// pass deleteFiles option through // pass deleteFiles option through
handleConfirmDelete(deleteFiles); handleConfirmDelete(deleteFiles);
}} }}
loading={deleting}
title={ title={
deleteTarget && deleteTarget.length > 1 deleteTarget && deleteTarget.length > 1
? 'Delete Multiple Logos' ? 'Delete Multiple Logos'
@ -633,6 +637,7 @@ export default function VODLogosTable() {
<ConfirmationDialog <ConfirmationDialog
opened={confirmCleanupOpen} opened={confirmCleanupOpen}
onClose={() => setConfirmCleanupOpen(false)} onClose={() => setConfirmCleanupOpen(false)}
loading={isCleaningUp}
onConfirm={handleConfirmCleanup} onConfirm={handleConfirmCleanup}
title="Cleanup Unused Logos" title="Cleanup Unused Logos"
message={ message={