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
- 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.
- 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)

View file

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

View file

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

View file

@ -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={
<div style={{ whiteSpace: 'pre-line' }}>

View file

@ -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 ? (

View file

@ -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 ? (

View file

@ -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={
<>
<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 [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 ? (

View file

@ -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 ? (

View file

@ -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 ? (

View file

@ -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 = () => {
<ConfirmationDialog
opened={confirmDeleteOpen}
onClose={() => setConfirmDeleteOpen(false)}
loading={isLoading}
onConfirm={(deleteFiles) => {
if (isBulkDelete) {
executeBulkDelete(deleteFiles);
@ -867,6 +868,7 @@ const LogosTable = () => {
<ConfirmationDialog
opened={confirmCleanupOpen}
onClose={() => setConfirmCleanupOpen(false)}
loading={isCleaningUp}
onConfirm={executeCleanupUnused}
title="Cleanup Unused Logos"
message={

View file

@ -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 ? (

View file

@ -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"
/>
</>

View file

@ -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 ? (

View file

@ -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() {
<ConfirmationDialog
opened={confirmCleanupOpen}
onClose={() => setConfirmCleanupOpen(false)}
loading={isCleaningUp}
onConfirm={handleConfirmCleanup}
title="Cleanup Unused Logos"
message={