mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-25 19:14:00 +00:00
Enhancement: Bulk assign custom dummy epgs.
Enhancement: Display a dynamic warning dialog when saving batch channel edits that will display what will be changed.
This commit is contained in:
parent
0fd464cb96
commit
2042274f10
1 changed files with 181 additions and 69 deletions
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|||
import useChannelsStore from '../../store/channels';
|
||||
import API from '../../api';
|
||||
import useStreamProfilesStore from '../../store/streamProfiles';
|
||||
import useEPGsStore from '../../store/epgs';
|
||||
import ChannelGroupForm from './ChannelGroup';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -53,6 +54,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
}, [ensureLogosLoaded]);
|
||||
|
||||
const streamProfiles = useStreamProfilesStore((s) => s.profiles);
|
||||
const epgs = useEPGsStore((s) => s.epgs);
|
||||
const fetchEPGs = useEPGsStore((s) => s.fetchEPGs);
|
||||
|
||||
const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false);
|
||||
const [selectedChannelGroup, setSelectedChannelGroup] = useState('-1');
|
||||
|
|
@ -60,6 +63,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [regexFind, setRegexFind] = useState('');
|
||||
const [regexReplace, setRegexReplace] = useState('');
|
||||
const [selectedDummyEpgId, setSelectedDummyEpgId] = useState(null);
|
||||
|
||||
const [groupPopoverOpened, setGroupPopoverOpened] = useState(false);
|
||||
const [groupFilter, setGroupFilter] = useState('');
|
||||
|
|
@ -71,10 +75,22 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
const [confirmSetNamesOpen, setConfirmSetNamesOpen] = useState(false);
|
||||
const [confirmSetLogosOpen, setConfirmSetLogosOpen] = useState(false);
|
||||
const [confirmSetTvgIdsOpen, setConfirmSetTvgIdsOpen] = useState(false);
|
||||
const [confirmClearEpgsOpen, setConfirmClearEpgsOpen] = useState(false);
|
||||
const [confirmBatchUpdateOpen, setConfirmBatchUpdateOpen] = useState(false);
|
||||
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
||||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
|
||||
// Fetch EPG sources when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchEPGs();
|
||||
}
|
||||
}, [isOpen, fetchEPGs]);
|
||||
|
||||
// Get dummy EPG sources
|
||||
const dummyEpgSources = useMemo(() => {
|
||||
return Object.values(epgs).filter((epg) => epg.source_type === 'dummy');
|
||||
}, [epgs]);
|
||||
|
||||
const form = useForm({
|
||||
mode: 'uncontrolled',
|
||||
initialValues: {
|
||||
|
|
@ -85,7 +101,88 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
},
|
||||
});
|
||||
|
||||
// Build confirmation message based on selected changes
|
||||
const getConfirmationMessage = () => {
|
||||
const changes = [];
|
||||
const values = form.getValues();
|
||||
|
||||
// Check for regex name changes
|
||||
if (regexFind.trim().length > 0) {
|
||||
changes.push(
|
||||
`• Name Change: Apply regex find "${regexFind}" replace with "${regexReplace || ''}"`
|
||||
);
|
||||
}
|
||||
|
||||
// Check channel group
|
||||
if (selectedChannelGroup && selectedChannelGroup !== '-1') {
|
||||
const groupName = channelGroups[selectedChannelGroup]?.name || 'Unknown';
|
||||
changes.push(`• Channel Group: ${groupName}`);
|
||||
}
|
||||
|
||||
// Check logo
|
||||
if (selectedLogoId && selectedLogoId !== '-1') {
|
||||
if (selectedLogoId === '0') {
|
||||
changes.push(`• Logo: Use Default`);
|
||||
} else {
|
||||
const logoName = channelLogos[selectedLogoId]?.name || 'Selected Logo';
|
||||
changes.push(`• Logo: ${logoName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check stream profile
|
||||
if (values.stream_profile_id && values.stream_profile_id !== '-1') {
|
||||
if (values.stream_profile_id === '0') {
|
||||
changes.push(`• Stream Profile: Use Default`);
|
||||
} else {
|
||||
const profileName =
|
||||
streamProfiles[values.stream_profile_id]?.name || 'Selected Profile';
|
||||
changes.push(`• Stream Profile: ${profileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check user level
|
||||
if (values.user_level && values.user_level !== '-1') {
|
||||
const userLevelLabel =
|
||||
USER_LEVEL_LABELS[values.user_level] || values.user_level;
|
||||
changes.push(`• User Level: ${userLevelLabel}`);
|
||||
}
|
||||
|
||||
// Check dummy EPG
|
||||
if (selectedDummyEpgId) {
|
||||
if (selectedDummyEpgId === 'clear') {
|
||||
changes.push(`• EPG: Clear Assignment (use default dummy)`);
|
||||
} else {
|
||||
const epgName = epgs[selectedDummyEpgId]?.name || 'Selected EPG';
|
||||
changes.push(`• Dummy EPG: ${epgName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const changes = getConfirmationMessage();
|
||||
|
||||
// If no changes detected, show notification
|
||||
if (changes.length === 0) {
|
||||
notifications.show({
|
||||
title: 'No Changes',
|
||||
message: 'Please select at least one field to update.',
|
||||
color: 'orange',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip warning if suppressed
|
||||
if (isWarningSuppressed('batch-update-channels')) {
|
||||
return onSubmit();
|
||||
}
|
||||
|
||||
setConfirmBatchUpdateOpen(true);
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
setConfirmBatchUpdateOpen(false);
|
||||
setIsSubmitting(true);
|
||||
|
||||
const values = {
|
||||
|
|
@ -126,6 +223,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
try {
|
||||
const applyRegex = regexFind.trim().length > 0;
|
||||
|
||||
// First, handle standard field updates (name, group, logo, etc.)
|
||||
if (applyRegex) {
|
||||
// Build per-channel updates to apply unique names via regex
|
||||
let flags = 'g';
|
||||
|
|
@ -153,10 +251,37 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
});
|
||||
|
||||
await API.bulkUpdateChannels(updates);
|
||||
} else {
|
||||
} else if (Object.keys(values).length > 0) {
|
||||
await API.updateChannels(channelIds, values);
|
||||
}
|
||||
|
||||
// Then, handle EPG assignment if a dummy EPG was selected
|
||||
if (selectedDummyEpgId) {
|
||||
if (selectedDummyEpgId === 'clear') {
|
||||
// Clear EPG assignments
|
||||
const associations = channelIds.map((id) => ({
|
||||
channel_id: id,
|
||||
epg_data_id: null,
|
||||
}));
|
||||
await API.batchSetEPG(associations);
|
||||
} else {
|
||||
// Assign the selected dummy EPG
|
||||
const selectedEpg = epgs[selectedDummyEpgId];
|
||||
if (
|
||||
selectedEpg &&
|
||||
selectedEpg.epg_data_ids &&
|
||||
selectedEpg.epg_data_ids.length > 0
|
||||
) {
|
||||
const epgDataId = selectedEpg.epg_data_ids[0];
|
||||
const associations = channelIds.map((id) => ({
|
||||
channel_id: id,
|
||||
epg_data_id: epgDataId,
|
||||
}));
|
||||
await API.batchSetEPG(associations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh both the channels table data and the main channels store
|
||||
await Promise.all([
|
||||
API.requeryChannels(),
|
||||
|
|
@ -305,49 +430,6 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClearEpgs = async () => {
|
||||
if (!channelIds || channelIds.length === 0) {
|
||||
notifications.show({
|
||||
title: 'No Channels Selected',
|
||||
message: 'No channels to update.',
|
||||
color: 'orange',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip warning if suppressed
|
||||
if (isWarningSuppressed('batch-clear-epgs')) {
|
||||
return executeClearEpgs();
|
||||
}
|
||||
|
||||
setConfirmClearEpgsOpen(true);
|
||||
};
|
||||
|
||||
const executeClearEpgs = async () => {
|
||||
try {
|
||||
// Clear EPG assignments (set to null/dummy) using existing batchSetEPG API
|
||||
const associations = channelIds.map((id) => ({
|
||||
channel_id: id,
|
||||
epg_data_id: null,
|
||||
}));
|
||||
|
||||
await API.batchSetEPG(associations);
|
||||
|
||||
// batchSetEPG already shows a notification and refreshes channels
|
||||
// Close the modal
|
||||
setConfirmClearEpgsOpen(false);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to clear EPG assignments:', error);
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Failed to clear EPG assignments.',
|
||||
color: 'red',
|
||||
});
|
||||
setConfirmClearEpgsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// // const sameStreamProfile = channels.every(
|
||||
// // (channel) => channel.stream_profile_id == channels[0].stream_profile_id
|
||||
|
|
@ -418,7 +500,7 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
}
|
||||
styles={{ hannontent: { '--mantine-color-body': '#27272A' } }}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Group justify="space-between" align="top">
|
||||
<Stack gap="5" style={{ flex: 1 }}>
|
||||
<Paper withBorder p="xs" radius="md">
|
||||
|
|
@ -484,20 +566,30 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
|
|||
Set TVG-IDs from EPG
|
||||
</Button>
|
||||
</Group>
|
||||
<Group gap="xs" wrap="nowrap" mt="xs">
|
||||
<Button
|
||||
<Divider my="xs" />
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={600}>
|
||||
Assign Dummy EPG
|
||||
</Text>
|
||||
<Select
|
||||
size="xs"
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={handleClearEpgs}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Clear EPG (Set to Dummy)
|
||||
</Button>
|
||||
</Group>
|
||||
placeholder="Select a dummy EPG..."
|
||||
data={[
|
||||
{ value: 'clear', label: '(Clear EPG Assignment)' },
|
||||
...dummyEpgSources.map((epg) => ({
|
||||
value: String(epg.id),
|
||||
label: epg.name,
|
||||
})),
|
||||
]}
|
||||
value={selectedDummyEpgId}
|
||||
onChange={setSelectedDummyEpgId}
|
||||
clearable
|
||||
/>
|
||||
</Stack>
|
||||
<Text size="xs" c="dimmed" mt="xs">
|
||||
Updates channel names, logos, and TVG-IDs based on their
|
||||
assigned EPG data, or clear EPG assignments to use dummy EPG
|
||||
assigned EPG data, or assign a custom dummy EPG to selected
|
||||
channels
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
|
|
@ -895,22 +987,42 @@ This action cannot be undone.`}
|
|||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={confirmClearEpgsOpen}
|
||||
onClose={() => setConfirmClearEpgsOpen(false)}
|
||||
onConfirm={executeClearEpgs}
|
||||
title="Confirm Clear EPG Assignments"
|
||||
opened={confirmBatchUpdateOpen}
|
||||
onClose={() => setConfirmBatchUpdateOpen(false)}
|
||||
onConfirm={onSubmit}
|
||||
title="Confirm Batch Update"
|
||||
message={
|
||||
<div style={{ whiteSpace: 'pre-line' }}>
|
||||
{`Are you sure you want to clear EPG assignments for ${channelIds?.length || 0} selected channels?
|
||||
|
||||
This will set all selected channels to use dummy EPG data.
|
||||
|
||||
This action cannot be undone.`}
|
||||
<div>
|
||||
<Text mb="md">
|
||||
You are about to apply the following changes to{' '}
|
||||
<strong>{channelIds?.length || 0}</strong> selected channel
|
||||
{(channelIds?.length || 0) !== 1 ? 's' : ''}:
|
||||
</Text>
|
||||
<Paper
|
||||
withBorder
|
||||
p="sm"
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)' }}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
{getConfirmationMessage().map((change, index) => (
|
||||
<Text
|
||||
key={index}
|
||||
size="sm"
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
>
|
||||
{change}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Text mt="md" size="sm" c="dimmed">
|
||||
This action cannot be undone.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
confirmLabel="Clear EPGs"
|
||||
confirmLabel="Apply Changes"
|
||||
cancelLabel="Cancel"
|
||||
actionKey="batch-clear-epgs"
|
||||
actionKey="batch-update-channels"
|
||||
onSuppressChange={suppressWarning}
|
||||
size="md"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue