diff --git a/frontend/src/components/ConfirmationDialog.jsx b/frontend/src/components/ConfirmationDialog.jsx new file mode 100644 index 00000000..3c0f15e7 --- /dev/null +++ b/frontend/src/components/ConfirmationDialog.jsx @@ -0,0 +1,77 @@ +import { Modal, Group, Text, Button, Checkbox, Box } from '@mantine/core'; +import React, { useState } from 'react'; +import useWarningsStore from '../store/warnings'; + +/** + * A reusable confirmation dialog with option to suppress future warnings + * + * @param {Object} props - Component props + * @param {boolean} props.opened - Whether the dialog is visible + * @param {Function} props.onClose - Function to call when closing without confirming + * @param {Function} props.onConfirm - Function to call when confirming the action + * @param {string} props.title - Dialog title + * @param {string} props.message - Dialog message + * @param {string} props.confirmLabel - Text for the confirm button + * @param {string} props.cancelLabel - Text for the cancel button + * @param {string} props.actionKey - Unique key for this type of action (used for suppression) + * @param {Function} props.onSuppressChange - Called when "don't show again" option changes + * @param {string} [props.size='md'] - Size of the modal + */ +const ConfirmationDialog = ({ + opened, + onClose, + onConfirm, + title = 'Confirm Action', + message = 'Are you sure you want to proceed?', + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + actionKey, + onSuppressChange, + size = 'md', // Add default size parameter - md is a medium width +}) => { + const suppressWarning = useWarningsStore((s) => s.suppressWarning); + const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); + const [suppressChecked, setSuppressChecked] = useState( + isWarningSuppressed(actionKey) + ); + + const handleToggleSuppress = (e) => { + setSuppressChecked(e.currentTarget.checked); + if (onSuppressChange) { + onSuppressChange(e.currentTarget.checked); + } + }; + + const handleConfirm = () => { + if (suppressChecked) { + suppressWarning(actionKey); + } + onConfirm(); + }; + + return ( + + {message} + + {actionKey && ( + + )} + + + + + + + ); +}; + +export default ConfirmationDialog; diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index b737e4b5..a1e1abb0 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -49,6 +49,8 @@ import useLocalStorage from '../../hooks/useLocalStorage'; import { CustomTable, useTable } from './CustomTable'; import ChannelsTableOnboarding from './ChannelsTable/ChannelsTableOnboarding'; import ChannelTableHeader from './ChannelsTable/ChannelTableHeader'; +import useWarningsStore from '../../store/warnings'; +import ConfirmationDialog from '../ConfirmationDialog'; const m3uUrlBase = `${window.location.protocol}//${window.location.host}/output/m3u`; const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/epg`; @@ -234,6 +236,10 @@ const ChannelsTable = ({ }) => { const showVideo = useVideoStore((s) => s.showVideo); const [tableSize, _] = useLocalStorage('table-size', 'default'); + // store/warnings + const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); + const suppressWarning = useWarningsStore((s) => s.suppressWarning); + /** * useMemo */ @@ -263,6 +269,11 @@ const ChannelsTable = ({ }) => { const [epgUrl, setEPGUrl] = useState(epgUrlBase); const [m3uUrl, setM3UUrl] = useState(m3uUrlBase); + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [isBulkDelete, setIsBulkDelete] = useState(false); + const [channelToDelete, setChannelToDelete] = useState(null); + /** * Dereived variables */ @@ -338,11 +349,58 @@ const ChannelsTable = ({ }) => { const deleteChannel = async (id) => { console.log(`Deleting channel with ID: ${id}`); table.setSelectedTableIds([]); + if (selectedChannelIds.length > 0) { - return deleteChannels(); + // Use bulk delete for multiple selections + setIsBulkDelete(true); + setChannelToDelete(null); + + if (isWarningSuppressed('delete-channels')) { + // Skip warning if suppressed + return executeDeleteChannels(); + } + + setConfirmDeleteOpen(true); + return; } + + // Single channel delete + setIsBulkDelete(false); + setDeleteTarget(id); + setChannelToDelete(channels[id]); // Store the channel object for displaying details + + if (isWarningSuppressed('delete-channel')) { + // Skip warning if suppressed + return executeDeleteChannel(id); + } + + setConfirmDeleteOpen(true); + }; + + const executeDeleteChannel = async (id) => { await API.deleteChannel(id); API.requeryChannels(); + setConfirmDeleteOpen(false); + }; + + const deleteChannels = async () => { + if (isWarningSuppressed('delete-channels')) { + // Skip warning if suppressed + return executeDeleteChannels(); + } + + setIsBulkDelete(true); + setConfirmDeleteOpen(true); + }; + + const executeDeleteChannels = async () => { + setIsLoading(true); + await API.deleteChannels(table.selectedTableIds); + await API.requeryChannels(); + setSelectedChannelIds([]); + table.setSelectedTableIds([]); + setIsLoading(false); + setConfirmDeleteOpen(false); }; const createRecording = (channel) => { @@ -412,16 +470,6 @@ const ChannelsTable = ({ }) => { [selectedProfileId] ); - // (Optional) bulk delete, but your endpoint is @TODO - const deleteChannels = async () => { - setIsLoading(true); - await API.deleteChannels(table.selectedTableIds); - await API.requeryChannels(); - setSelectedChannelIds([]); - table.setSelectedTableIds([]); - setIsLoading(false); - }; - const closeChannelForm = () => { setChannel(null); setChannelModalOpen(false); @@ -755,236 +803,264 @@ const ChannelsTable = ({ }) => { const rows = table.getRowModel().rows; return ( - - {/* Header Row: outside the Paper */} - - - Channels - - + <> + + {/* Header Row: outside the Paper */} + - Links: + Channels - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Paper container: contains top toolbar and table (or ghost state) */} - - - - {/* Table or ghost empty state inside Paper */} - - {Object.keys(channels).length === 0 && ( - - )} - - - {Object.keys(channels).length > 0 && ( - - - - + Links: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Paper container: contains top toolbar and table (or ghost state) */} + + + + {/* Table or ghost empty state inside Paper */} + + {Object.keys(channels).length === 0 && ( + + )} + + + {Object.keys(channels).length > 0 && ( - - Page Size - - - {paginationString} - + + + + + + Page Size + + + {paginationString} + + - - )} - + )} + - + - + + + setConfirmDeleteOpen(false)} + onConfirm={() => isBulkDelete ? executeDeleteChannels() : executeDeleteChannel(deleteTarget)} + title={`Confirm ${isBulkDelete ? 'Bulk ' : ''}Channel Deletion`} + message={ + isBulkDelete + ? `Are you sure you want to delete ${table.selectedTableIds.length} channels? This action cannot be undone.` + : channelToDelete + ?
+ {`Are you sure you want to delete the following channel? + +Name: ${channelToDelete.name} +Channel Number: ${channelToDelete.channel_number} + +This action cannot be undone.`} +
+ : "Are you sure you want to delete this channel? This action cannot be undone." + } + confirmLabel="Delete" + cancelLabel="Cancel" + actionKey={isBulkDelete ? 'delete-channels' : 'delete-channel'} + onSuppressChange={suppressWarning} + size="md" /> - + ); }; diff --git a/frontend/src/store/warnings.jsx b/frontend/src/store/warnings.jsx new file mode 100644 index 00000000..3a57b21f --- /dev/null +++ b/frontend/src/store/warnings.jsx @@ -0,0 +1,29 @@ +import { create } from 'zustand'; + +const useWarningsStore = create((set) => ({ + // Map of action keys to whether they're suppressed + suppressedWarnings: {}, + + // Function to check if a warning is suppressed + isWarningSuppressed: (actionKey) => { + const state = useWarningsStore.getState(); + return state.suppressedWarnings[actionKey] === true; + }, + + // Function to suppress a warning + suppressWarning: (actionKey, suppressed = true) => { + set((state) => ({ + suppressedWarnings: { + ...state.suppressedWarnings, + [actionKey]: suppressed + } + })); + }, + + // Function to reset all suppressions + resetSuppressions: () => { + set({ suppressedWarnings: {} }); + } +})); + +export default useWarningsStore;