@@ -1010,6 +1022,7 @@ This action cannot be undone.`}
opened={confirmBatchUpdateOpen}
onClose={() => setConfirmBatchUpdateOpen(false)}
onConfirm={onSubmit}
+ loading={isSubmitting}
title="Confirm Batch Update"
message={
diff --git a/frontend/src/components/forms/GroupManager.jsx b/frontend/src/components/forms/GroupManager.jsx
index adcd55d3..453d9494 100644
--- a/frontend/src/components/forms/GroupManager.jsx
+++ b/frontend/src/components/forms/GroupManager.jsx
@@ -183,6 +183,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [groupToDelete, setGroupToDelete] = useState(null);
const [confirmCleanupOpen, setConfirmCleanupOpen] = useState(false);
+ const [deletingGroup, setDeletingGroup] = useState(false);
// Memoize the channel groups array to prevent unnecessary re-renders
const channelGroupsArray = useMemo(
@@ -382,6 +383,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
const executeDeleteGroup = useCallback(
async (group) => {
+ setDeletingGroup(true);
try {
await API.deleteChannelGroup(group.id);
@@ -392,13 +394,14 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
});
await fetchGroupUsage(); // Refresh usage data
- setConfirmDeleteOpen(false);
} catch (error) {
notifications.show({
title: 'Error',
message: 'Failed to delete group',
color: 'red',
});
+ } finally {
+ setDeletingGroup(false);
setConfirmDeleteOpen(false);
}
},
@@ -680,6 +683,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => groupToDelete && executeDeleteGroup(groupToDelete)}
+ loading={deletingGroup}
title="Confirm Group Deletion"
message={
groupToDelete ? (
@@ -706,6 +710,7 @@ This action cannot be undone.`}
opened={confirmCleanupOpen}
onClose={() => setConfirmCleanupOpen(false)}
onConfirm={executeCleanup}
+ loading={isCleaningUp}
title="Confirm Group Cleanup"
message={
diff --git a/frontend/src/components/forms/M3UFilters.jsx b/frontend/src/components/forms/M3UFilters.jsx
index f8bf9975..acd0c446 100644
--- a/frontend/src/components/forms/M3UFilters.jsx
+++ b/frontend/src/components/forms/M3UFilters.jsx
@@ -151,6 +151,7 @@ const M3UFilters = ({ playlist, isOpen, onClose }) => {
const [deleteTarget, setDeleteTarget] = useState(null);
const [filterToDelete, setFilterToDelete] = useState(null);
const [filters, setFilters] = useState([]);
+ const [deleting, setDeleting] = useState(false);
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
@@ -192,16 +193,17 @@ const M3UFilters = ({ playlist, isOpen, onClose }) => {
const deleteFilter = async (id) => {
if (!playlist || !playlist.id) return;
+ setDeleting(true);
try {
await API.deleteM3UFilter(playlist.id, id);
- setConfirmDeleteOpen(false);
+ fetchPlaylist(playlist.id);
+ setFilters(filters.filter((f) => f.id !== id));
} catch (error) {
console.error('Error deleting profile:', error);
+ } finally {
+ setDeleting(false);
setConfirmDeleteOpen(false);
}
-
- fetchPlaylist(playlist.id);
- setFilters(filters.filter((f) => f.id !== id));
};
const closeEditor = (updatedPlaylist = null) => {
@@ -321,6 +323,7 @@ const M3UFilters = ({ playlist, isOpen, onClose }) => {
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => deleteFilter(deleteTarget)}
+ loading={deleting}
title="Confirm Filter Deletion"
message={
filterToDelete ? (
diff --git a/frontend/src/components/forms/M3UProfiles.jsx b/frontend/src/components/forms/M3UProfiles.jsx
index 88caf49f..efbc1ed8 100644
--- a/frontend/src/components/forms/M3UProfiles.jsx
+++ b/frontend/src/components/forms/M3UProfiles.jsx
@@ -38,6 +38,7 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
const [profileToDelete, setProfileToDelete] = useState(null);
+ const [deletingProfile, setDeletingProfile] = useState(false);
const [accountInfoOpen, setAccountInfoOpen] = useState(false);
const [selectedProfileForInfo, setSelectedProfileForInfo] = useState(null);
@@ -88,11 +89,13 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
const executeDeleteProfile = async (id) => {
if (!playlist || !playlist.id) return;
+ setDeletingProfile(true);
try {
await API.deleteM3UProfile(playlist.id, id);
- setConfirmDeleteOpen(false);
} catch (error) {
console.error('Error deleting profile:', error);
+ } finally {
+ setDeletingProfile(false);
setConfirmDeleteOpen(false);
}
};
@@ -359,6 +362,7 @@ const M3UProfiles = ({ playlist = null, isOpen, onClose }) => {
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeleteProfile(deleteTarget)}
+ loading={deletingProfile}
title="Confirm Profile Deletion"
message={
profileToDelete ? (
diff --git a/frontend/src/components/forms/settings/NetworkAccessForm.jsx b/frontend/src/components/forms/settings/NetworkAccessForm.jsx
index 1d2c42e7..8200d635 100644
--- a/frontend/src/components/forms/settings/NetworkAccessForm.jsx
+++ b/frontend/src/components/forms/settings/NetworkAccessForm.jsx
@@ -20,6 +20,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
const [saved, setSaved] = useState(false);
const [networkAccessConfirmOpen, setNetworkAccessConfirmOpen] =
useState(false);
+ const [saving, setSaving] = useState(false);
const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] =
useState([]);
const [clientIpAddress, setClientIpAddress] = useState(null);
@@ -31,7 +32,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
});
useEffect(() => {
- if(!active) setSaved(false);
+ if (!active) setSaved(false);
}, [active]);
useEffect(() => {
@@ -74,19 +75,22 @@ const NetworkAccessForm = React.memo(({ active }) => {
const saveNetworkAccess = async () => {
setSaved(false);
+ setSaving(true);
try {
await updateSetting({
...settings['network-access'],
value: JSON.stringify(networkAccessForm.getValues()),
});
setSaved(true);
- setNetworkAccessConfirmOpen(false);
} catch (e) {
const errors = {};
for (const key in e.body.value) {
errors[key] = `Invalid CIDR(s): ${e.body.value[key]}`;
}
networkAccessForm.setErrors(errors);
+ } finally {
+ setSaving(false);
+ setNetworkAccessConfirmOpen(false);
}
};
@@ -135,6 +139,7 @@ const NetworkAccessForm = React.memo(({ active }) => {
onClose={() => setNetworkAccessConfirmOpen(false)}
onConfirm={saveNetworkAccess}
title={`Confirm Network Access Blocks`}
+ loading={saving}
message={
<>
@@ -158,4 +163,4 @@ const NetworkAccessForm = React.memo(({ active }) => {
);
});
-export default NetworkAccessForm;
\ No newline at end of file
+export default NetworkAccessForm;
diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx
index cbfb510c..dc82c131 100644
--- a/frontend/src/components/tables/ChannelsTable.jsx
+++ b/frontend/src/components/tables/ChannelsTable.jsx
@@ -316,6 +316,7 @@ const ChannelsTable = ({ onReady }) => {
const [deleteTarget, setDeleteTarget] = useState(null);
const [isBulkDelete, setIsBulkDelete] = useState(false);
const [channelToDelete, setChannelToDelete] = useState(null);
+ const [deleting, setDeleting] = useState(false);
const hasFetchedData = useRef(false);
@@ -545,9 +546,14 @@ const ChannelsTable = ({ onReady }) => {
};
const executeDeleteChannel = async (id) => {
- await API.deleteChannel(id);
- API.requeryChannels();
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deleteChannel(id);
+ API.requeryChannels();
+ } finally {
+ setDeleting(false);
+ setConfirmDeleteOpen(false);
+ }
};
const deleteChannels = async () => {
@@ -562,12 +568,17 @@ const ChannelsTable = ({ onReady }) => {
const executeDeleteChannels = async () => {
setIsLoading(true);
- await API.deleteChannels(table.selectedTableIds);
- await API.requeryChannels();
- setSelectedChannelIds([]);
- table.setSelectedTableIds([]);
- setIsLoading(false);
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deleteChannels(table.selectedTableIds);
+ await API.requeryChannels();
+ setSelectedChannelIds([]);
+ table.setSelectedTableIds([]);
+ } finally {
+ setDeleting(false);
+ setIsLoading(false);
+ setConfirmDeleteOpen(false);
+ }
};
const createRecording = (channel) => {
@@ -1498,6 +1509,7 @@ const ChannelsTable = ({ onReady }) => {
? executeDeleteChannels()
: executeDeleteChannel(deleteTarget)
}
+ loading={deleting}
title={`Confirm ${isBulkDelete ? 'Bulk ' : ''}Channel Deletion`}
message={
isBulkDelete ? (
diff --git a/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx b/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx
index de389ccb..2263806f 100644
--- a/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx
+++ b/frontend/src/components/tables/ChannelsTable/ChannelTableHeader.jsx
@@ -118,6 +118,7 @@ const ChannelTableHeader = ({
const [confirmDeleteProfileOpen, setConfirmDeleteProfileOpen] =
useState(false);
const [profileToDelete, setProfileToDelete] = useState(null);
+ const [deletingProfile, setDeletingProfile] = useState(false);
const [profileModalState, setProfileModalState] = useState({
opened: false,
mode: null,
@@ -157,8 +158,13 @@ const ChannelTableHeader = ({
};
const executeDeleteProfile = async (id) => {
- await API.deleteChannelProfile(id);
- setConfirmDeleteProfileOpen(false);
+ setDeletingProfile(true);
+ try {
+ await API.deleteChannelProfile(id);
+ } finally {
+ setDeletingProfile(false);
+ setConfirmDeleteProfileOpen(false);
+ }
};
const matchEpg = async () => {
@@ -402,6 +408,7 @@ const ChannelTableHeader = ({
opened={confirmDeleteProfileOpen}
onClose={() => setConfirmDeleteProfileOpen(false)}
onConfirm={() => executeDeleteProfile(profileToDelete?.id)}
+ loading={deletingProfile}
title="Confirm Profile Deletion"
message={
profileToDelete ? (
diff --git a/frontend/src/components/tables/EPGsTable.jsx b/frontend/src/components/tables/EPGsTable.jsx
index b8dfeb6d..f6952542 100644
--- a/frontend/src/components/tables/EPGsTable.jsx
+++ b/frontend/src/components/tables/EPGsTable.jsx
@@ -110,6 +110,7 @@ const EPGsTable = () => {
const [deleteTarget, setDeleteTarget] = useState(null);
const [epgToDelete, setEpgToDelete] = useState(null);
const [data, setData] = useState([]);
+ const [deleting, setDeleting] = useState(false);
const epgs = useEPGsStore((s) => s.epgs);
const refreshProgress = useEPGsStore((s) => s.refreshProgress);
@@ -431,10 +432,13 @@ const EPGsTable = () => {
};
const executeDeleteEPG = async (id) => {
- setIsLoading(true);
- await API.deleteEPG(id);
- setIsLoading(false);
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deleteEPG(id);
+ } finally {
+ setDeleting(false);
+ setConfirmDeleteOpen(false);
+ }
};
const refreshEPG = async (id) => {
@@ -688,6 +692,7 @@ const EPGsTable = () => {
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeleteEPG(deleteTarget)}
+ loading={deleting}
title="Confirm EPG Source Deletion"
message={
epgToDelete ? (
diff --git a/frontend/src/components/tables/LogosTable.jsx b/frontend/src/components/tables/LogosTable.jsx
index 3e278ebd..7c718590 100644
--- a/frontend/src/components/tables/LogosTable.jsx
+++ b/frontend/src/components/tables/LogosTable.jsx
@@ -189,12 +189,12 @@ const LogosTable = () => {
color: 'red',
});
} finally {
- setIsLoading(false);
setConfirmDeleteOpen(false);
setDeleteTarget(null);
setLogoToDelete(null);
setIsBulkDelete(false);
clearSelections(); // Clear selections
+ setIsLoading(false);
}
},
[fetchAllLogos, clearSelections]
@@ -221,10 +221,10 @@ const LogosTable = () => {
color: 'red',
});
} finally {
- setIsLoading(false);
setConfirmDeleteOpen(false);
setIsBulkDelete(false);
clearSelections(); // Clear selections
+ setIsLoading(false);
}
},
[selectedRows, fetchAllLogos, clearSelections]
@@ -805,6 +805,7 @@ const LogosTable = () => {
setConfirmDeleteOpen(false)}
+ loading={isLoading}
onConfirm={(deleteFiles) => {
if (isBulkDelete) {
executeBulkDelete(deleteFiles);
@@ -867,6 +868,7 @@ const LogosTable = () => {
setConfirmCleanupOpen(false)}
+ loading={isCleaningUp}
onConfirm={executeCleanupUnused}
title="Cleanup Unused Logos"
message={
diff --git a/frontend/src/components/tables/M3UsTable.jsx b/frontend/src/components/tables/M3UsTable.jsx
index 64b401d8..6e88af56 100644
--- a/frontend/src/components/tables/M3UsTable.jsx
+++ b/frontend/src/components/tables/M3UsTable.jsx
@@ -140,6 +140,7 @@ const M3UTable = () => {
const [playlistToDelete, setPlaylistToDelete] = useState(null);
const [data, setData] = useState([]);
const [sorting, setSorting] = useState([{ id: 'name', desc: '' }]);
+ const [deleting, setDeleting] = useState(false);
const playlists = usePlaylistsStore((s) => s.playlists);
const refreshProgress = usePlaylistsStore((s) => s.refreshProgress);
@@ -400,9 +401,14 @@ const M3UTable = () => {
const executeDeletePlaylist = async (id) => {
setIsLoading(true);
- await API.deletePlaylist(id);
- setIsLoading(false);
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deletePlaylist(id);
+ } finally {
+ setDeleting(false);
+ setIsLoading(false);
+ setConfirmDeleteOpen(false);
+ }
};
const toggleActive = async (playlist) => {
@@ -893,6 +899,7 @@ const M3UTable = () => {
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeletePlaylist(deleteTarget)}
+ loading={deleting}
title="Confirm M3U Account Deletion"
message={
playlistToDelete ? (
diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx
index c39e63ab..21b13baf 100644
--- a/frontend/src/components/tables/StreamsTable.jsx
+++ b/frontend/src/components/tables/StreamsTable.jsx
@@ -215,6 +215,7 @@ const StreamsTable = ({ onReady }) => {
const [deleteTarget, setDeleteTarget] = useState(null);
const [streamToDelete, setStreamToDelete] = useState(null);
const [isBulkDelete, setIsBulkDelete] = useState(false);
+ const [deleting, setDeleting] = useState(false);
// const [allRowsSelected, setAllRowsSelected] = useState(false);
@@ -590,12 +591,17 @@ const StreamsTable = ({ onReady }) => {
};
const executeDeleteStream = async (id) => {
- await API.deleteStream(id);
- fetchData();
- // Clear the selection for the deleted stream
- setSelectedStreamIds([]);
- table.setSelectedTableIds([]);
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deleteStream(id);
+ fetchData();
+ // Clear the selection for the deleted stream
+ setSelectedStreamIds([]);
+ table.setSelectedTableIds([]);
+ } finally {
+ setDeleting(false);
+ setConfirmDeleteOpen(false);
+ }
};
const deleteStreams = async () => {
@@ -612,12 +618,17 @@ const StreamsTable = ({ onReady }) => {
const executeDeleteStreams = async () => {
setIsLoading(true);
- await API.deleteStreams(selectedStreamIds);
- setIsLoading(false);
- fetchData();
- setSelectedStreamIds([]);
- table.setSelectedTableIds([]);
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deleteStreams(selectedStreamIds);
+ fetchData();
+ setSelectedStreamIds([]);
+ table.setSelectedTableIds([]);
+ } finally {
+ setDeleting(false);
+ setIsLoading(false);
+ setConfirmDeleteOpen(false);
+ }
};
const closeStreamForm = () => {
@@ -1258,6 +1269,7 @@ This action cannot be undone.`}
cancelLabel="Cancel"
actionKey={isBulkDelete ? 'delete-streams' : 'delete-stream'}
onSuppressChange={suppressWarning}
+ loading={deleting}
size="md"
/>
>
diff --git a/frontend/src/components/tables/UsersTable.jsx b/frontend/src/components/tables/UsersTable.jsx
index 3e9e4971..467423dc 100644
--- a/frontend/src/components/tables/UsersTable.jsx
+++ b/frontend/src/components/tables/UsersTable.jsx
@@ -96,6 +96,7 @@ const UsersTable = () => {
const [deleteTarget, setDeleteTarget] = useState(null);
const [userToDelete, setUserToDelete] = useState(null);
const [isLoading, setIsLoading] = useState(false);
+ const [deleting, setDeleting] = useState(false);
const [visiblePasswords, setVisiblePasswords] = useState({});
/**
@@ -110,9 +111,14 @@ const UsersTable = () => {
const executeDeleteUser = useCallback(async (id) => {
setIsLoading(true);
- await API.deleteUser(id);
- setIsLoading(false);
- setConfirmDeleteOpen(false);
+ setDeleting(true);
+ try {
+ await API.deleteUser(id);
+ } finally {
+ setDeleting(false);
+ setIsLoading(false);
+ setConfirmDeleteOpen(false);
+ }
}, []);
const editUser = useCallback(async (user = null) => {
@@ -406,6 +412,7 @@ const UsersTable = () => {
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
onConfirm={() => executeDeleteUser(deleteTarget)}
+ loading={deleting}
title="Confirm User Deletion"
message={
userToDelete ? (
diff --git a/frontend/src/components/tables/VODLogosTable.jsx b/frontend/src/components/tables/VODLogosTable.jsx
index ed166b65..75b322f5 100644
--- a/frontend/src/components/tables/VODLogosTable.jsx
+++ b/frontend/src/components/tables/VODLogosTable.jsx
@@ -74,6 +74,7 @@ export default function VODLogosTable() {
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState(null);
const [confirmCleanupOpen, setConfirmCleanupOpen] = useState(false);
+ const [deleting, setDeleting] = useState(false);
const [paginationString, setPaginationString] = useState('');
const [isCleaningUp, setIsCleaningUp] = useState(false);
const tableRef = React.useRef(null);
@@ -139,6 +140,7 @@ export default function VODLogosTable() {
}, []);
const handleConfirmDelete = async () => {
+ setDeleting(true);
try {
if (deleteTarget.length === 1) {
await deleteVODLogo(deleteTarget[0]);
@@ -162,6 +164,7 @@ export default function VODLogosTable() {
color: 'red',
});
} finally {
+ setDeleting(false);
// Always clear selections and close dialog, even on error
clearSelections();
setConfirmDeleteOpen(false);
@@ -571,6 +574,7 @@ export default function VODLogosTable() {
// pass deleteFiles option through
handleConfirmDelete(deleteFiles);
}}
+ loading={deleting}
title={
deleteTarget && deleteTarget.length > 1
? 'Delete Multiple Logos'
@@ -633,6 +637,7 @@ export default function VODLogosTable() {
setConfirmCleanupOpen(false)}
+ loading={isCleaningUp}
onConfirm={handleConfirmCleanup}
title="Cleanup Unused Logos"
message={