Refactor/Enhancement: Refactored channel numbering dialogs into a unified CreateChannelModal component that now includes channel profile selection alongside channel number assignment for both single and bulk channel creation. Users can choose to add channels to all profiles, no profiles, or specific profiles with mutual exclusivity between special options ("All Profiles", "None") and specific profile selections. Profile selection defaults to the current table filter for intuitive workflow.

This commit is contained in:
SergeantPanda 2026-01-11 19:05:07 -06:00
parent 6b873be3cf
commit b8374fcc68
4 changed files with 409 additions and 151 deletions

View file

@ -0,0 +1,122 @@
import React from 'react';
import {
Modal,
Stack,
Text,
Radio,
NumberInput,
Checkbox,
Group,
Button,
} from '@mantine/core';
const ChannelNumberingModal = ({
opened,
onClose,
mode,
onModeChange,
numberValue,
onNumberValueChange,
rememberChoice,
onRememberChoiceChange,
onConfirm,
// Props for customizing the modal behavior
isBulk = false,
streamCount = 1,
streamName = '',
}) => {
const title = isBulk
? 'Channel Numbering Options'
: 'Channel Number Assignment';
const confirmLabel = isBulk ? 'Create Channels' : 'Create Channel';
const numberingLabel = isBulk ? 'Numbering Mode' : 'Number Assignment';
// For bulk: use 'custom' mode, for single: use 'specific' mode
const customModeValue = isBulk ? 'custom' : 'specific';
return (
<Modal opened={opened} onClose={onClose} title={title} size="md" centered>
<Stack spacing="md">
<Text size="sm" c="dimmed">
{isBulk
? `Choose how to assign channel numbers to the ${streamCount} selected streams:`
: `Choose how to assign the channel number for "${streamName}":`}
</Text>
<Radio.Group
value={mode}
onChange={onModeChange}
label={numberingLabel}
>
<Stack mt="xs" spacing="xs">
<Radio
value="provider"
label={isBulk ? 'Use Provider Numbers' : 'Use Provider Number'}
description={
isBulk
? 'Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts'
: 'Use tvg-chno or channel-number from stream metadata, auto-assign if not available'
}
/>
<Radio
value="auto"
label={
isBulk ? 'Auto-Assign Sequential' : 'Auto-Assign Next Available'
}
description={
isBulk
? 'Start from the lowest available channel number and increment by 1'
: 'Automatically assign the next available channel number'
}
/>
<Radio
value={customModeValue}
label={
isBulk ? 'Start from Custom Number' : 'Use Specific Number'
}
description={
isBulk
? 'Start sequential numbering from a specific channel number'
: 'Use a specific channel number'
}
/>
</Stack>
</Radio.Group>
{mode === customModeValue && (
<NumberInput
label={isBulk ? 'Starting Channel Number' : 'Channel Number'}
description={
isBulk
? 'Channel numbers will be assigned starting from this number'
: 'The specific channel number to assign'
}
value={numberValue}
onChange={onNumberValueChange}
min={1}
placeholder={
isBulk ? 'Enter starting number...' : 'Enter channel number...'
}
/>
)}
<Checkbox
checked={rememberChoice}
onChange={(event) =>
onRememberChoiceChange(event.currentTarget.checked)
}
label="Remember this choice and don't ask again"
/>
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={onClose}>
Cancel
</Button>
<Button onClick={onConfirm}>{confirmLabel}</Button>
</Group>
</Stack>
</Modal>
);
};
export default ChannelNumberingModal;

View file

@ -0,0 +1,180 @@
import React from 'react';
import {
Modal,
Stack,
Text,
Radio,
NumberInput,
Checkbox,
Group,
Button,
MultiSelect,
Divider,
} from '@mantine/core';
const CreateChannelModal = ({
opened,
onClose,
mode,
onModeChange,
numberValue,
onNumberValueChange,
rememberChoice,
onRememberChoiceChange,
onConfirm,
// Props for customizing the modal behavior
isBulk = false,
streamCount = 1,
streamName = '',
// Channel profile props
selectedProfileIds,
onProfileIdsChange,
channelProfiles = [],
}) => {
const title = isBulk ? 'Create Channels Options' : 'Create Channel';
const confirmLabel = isBulk ? 'Create Channels' : 'Create Channel';
const numberingLabel = isBulk ? 'Numbering Mode' : 'Number Assignment';
// For bulk: use 'custom' mode, for single: use 'specific' mode
const customModeValue = isBulk ? 'custom' : 'specific';
// Convert channel profiles to MultiSelect data format with groups
// Filter out the "All" profile (id '0') and add our own special options
const profileOptions = [
{
group: 'Special',
items: [
{ value: 'all', label: 'All Profiles' },
{ value: 'none', label: 'No Profiles' },
],
},
{
group: 'Profiles',
items: channelProfiles
.filter((profile) => profile.id.toString() !== '0')
.map((profile) => ({
value: profile.id.toString(),
label: profile.name,
})),
},
];
// Handle profile selection with mutual exclusivity
const handleProfileChange = (newValue) => {
const lastSelected = newValue[newValue.length - 1];
// If 'all' or 'none' was just selected, clear everything else and keep only that
if (lastSelected === 'all' || lastSelected === 'none') {
onProfileIdsChange([lastSelected]);
}
// If a specific profile was selected, remove 'all' and 'none'
else if (newValue.includes('all') || newValue.includes('none')) {
onProfileIdsChange(newValue.filter((v) => v !== 'all' && v !== 'none'));
}
// Otherwise just update normally
else {
onProfileIdsChange(newValue);
}
};
return (
<Modal opened={opened} onClose={onClose} title={title} size="md" centered>
<Stack spacing="md">
<Text size="sm" c="dimmed">
{isBulk
? `Configure options for creating ${streamCount} channels from selected streams:`
: `Configure options for creating a channel from "${streamName}":`}
</Text>
<Divider label="Channel Profiles" labelPosition="left" />
<MultiSelect
label="Channel Profiles"
description="Select 'All Profiles' to add to all profiles, 'No Profiles' to not add to any profile, or choose specific profiles"
placeholder="Select profiles..."
data={profileOptions}
value={selectedProfileIds}
onChange={handleProfileChange}
searchable
clearable
/>
<Divider label="Channel Number" labelPosition="left" />
<Radio.Group
value={mode}
onChange={onModeChange}
label={numberingLabel}
>
<Stack mt="xs" spacing="xs">
<Radio
value="provider"
label={isBulk ? 'Use Provider Numbers' : 'Use Provider Number'}
description={
isBulk
? 'Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts'
: 'Use tvg-chno or channel-number from stream metadata, auto-assign if not available'
}
/>
<Radio
value="auto"
label={
isBulk ? 'Auto-Assign Sequential' : 'Auto-Assign Next Available'
}
description={
isBulk
? 'Start from the lowest available channel number and increment by 1'
: 'Automatically assign the next available channel number'
}
/>
<Radio
value={customModeValue}
label={
isBulk ? 'Start from Custom Number' : 'Use Specific Number'
}
description={
isBulk
? 'Start sequential numbering from a specific channel number'
: 'Use a specific channel number'
}
/>
</Stack>
</Radio.Group>
{mode === customModeValue && (
<NumberInput
label={isBulk ? 'Starting Channel Number' : 'Channel Number'}
description={
isBulk
? 'Channel numbers will be assigned starting from this number'
: 'The specific channel number to assign'
}
value={numberValue}
onChange={onNumberValueChange}
min={1}
placeholder={
isBulk ? 'Enter starting number...' : 'Enter channel number...'
}
/>
)}
<Checkbox
checked={rememberChoice}
onChange={(event) =>
onRememberChoiceChange(event.currentTarget.checked)
}
label="Remember this choice and don't ask again"
/>
<Group justify="flex-end" mt="md">
<Button variant="default" onClick={onClose}>
Cancel
</Button>
<Button onClick={onConfirm}>{confirmLabel}</Button>
</Group>
</Stack>
</Modal>
);
};
export default CreateChannelModal;

View file

@ -58,6 +58,7 @@ import useWarningsStore from '../../store/warnings';
import { CustomTable, useTable } from './CustomTable';
import useLocalStorage from '../../hooks/useLocalStorage';
import ConfirmationDialog from '../ConfirmationDialog';
import CreateChannelModal from '../modals/CreateChannelModal';
const StreamRowActions = ({
theme,
@ -193,19 +194,21 @@ const StreamsTable = ({ onReady }) => {
const [sorting, setSorting] = useState([{ id: 'name', desc: false }]);
const [selectedStreamIds, setSelectedStreamIds] = useState([]);
// Channel numbering modal state
// Channel creation modal state (bulk)
const [channelNumberingModalOpen, setChannelNumberingModalOpen] =
useState(false);
const [numberingMode, setNumberingMode] = useState('provider'); // 'provider', 'auto', or 'custom'
const [customStartNumber, setCustomStartNumber] = useState(1);
const [rememberChoice, setRememberChoice] = useState(false);
const [bulkSelectedProfileIds, setBulkSelectedProfileIds] = useState([]);
// Single channel numbering modal state
// Channel creation modal state (single)
const [singleChannelModalOpen, setSingleChannelModalOpen] = useState(false);
const [singleChannelMode, setSingleChannelMode] = useState('provider'); // 'provider', 'auto', or 'specific'
const [specificChannelNumber, setSpecificChannelNumber] = useState(1);
const [rememberSingleChoice, setRememberSingleChoice] = useState(false);
const [currentStreamForChannel, setCurrentStreamForChannel] = useState(null);
const [singleSelectedProfileIds, setSingleSelectedProfileIds] = useState([]);
// Confirmation dialog state
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
@ -260,6 +263,8 @@ const StreamsTable = ({ onReady }) => {
(state) =>
state.channels.find((chan) => chan.id === selectedChannelIds[0])?.streams
);
const channelProfiles = useChannelsStore((s) => s.profiles);
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
const env_mode = useSettingsStore((s) => s.environment.env_mode);
const showVideo = useVideoStore((s) => s.showVideo);
const [tableSize, _] = useLocalStorage('table-size', 'default');
@ -462,6 +467,11 @@ const StreamsTable = ({ onReady }) => {
const createChannelsFromStreams = async () => {
if (selectedStreamIds.length === 0) return;
// Set default profile selection based on current profile filter
const defaultProfileIds =
selectedProfileId === '0' ? ['all'] : [selectedProfileId];
setBulkSelectedProfileIds(defaultProfileIds);
// Check if user has suppressed the channel numbering dialog
const actionKey = 'channel-numbering-choice';
if (isWarningSuppressed(actionKey)) {
@ -478,7 +488,10 @@ const StreamsTable = ({ onReady }) => {
? 0
: Number(savedStartNumber);
await executeChannelCreation(startingChannelNumberValue);
await executeChannelCreation(
startingChannelNumberValue,
defaultProfileIds
);
} else {
// Show the modal to let user choose
setChannelNumberingModalOpen(true);
@ -486,15 +499,32 @@ const StreamsTable = ({ onReady }) => {
};
// Separate function to actually execute the channel creation
const executeChannelCreation = async (startingChannelNumberValue) => {
const executeChannelCreation = async (
startingChannelNumberValue,
profileIds = null
) => {
try {
const selectedChannelProfileId =
useChannelsStore.getState().selectedProfileId;
// Convert profile selection: 'all' means all profiles (null), 'none' means no profiles ([]), specific IDs otherwise
let channelProfileIds;
if (profileIds) {
if (profileIds.includes('none')) {
channelProfileIds = [];
} else if (profileIds.includes('all')) {
channelProfileIds = null;
} else {
channelProfileIds = profileIds
.filter((id) => id !== 'all' && id !== 'none')
.map((id) => parseInt(id));
}
} else {
channelProfileIds =
selectedProfileId !== '0' ? [parseInt(selectedProfileId)] : null;
}
// Use the async API for all bulk operations
const response = await API.createChannelsFromStreamsAsync(
selectedStreamIds,
selectedChannelProfileId !== '0' ? [selectedChannelProfileId] : null,
channelProfileIds,
startingChannelNumberValue
);
@ -533,7 +563,10 @@ const StreamsTable = ({ onReady }) => {
: Number(customStartNumber);
setChannelNumberingModalOpen(false);
await executeChannelCreation(startingChannelNumberValue);
await executeChannelCreation(
startingChannelNumberValue,
bulkSelectedProfileIds
);
};
const editStream = async (stream = null) => {
@ -595,6 +628,11 @@ const StreamsTable = ({ onReady }) => {
// Single channel creation functions
const createChannelFromStream = async (stream) => {
// Set default profile selection based on current profile filter
const defaultProfileIds =
selectedProfileId === '0' ? ['all'] : [selectedProfileId];
setSingleSelectedProfileIds(defaultProfileIds);
// Check if user has suppressed the single channel numbering dialog
const actionKey = 'single-channel-numbering-choice';
if (isWarningSuppressed(actionKey)) {
@ -611,7 +649,11 @@ const StreamsTable = ({ onReady }) => {
? 0
: Number(savedChannelNumber);
await executeSingleChannelCreation(stream, channelNumberValue);
await executeSingleChannelCreation(
stream,
channelNumberValue,
defaultProfileIds
);
} else {
// Show the modal to let user choose
setCurrentStreamForChannel(stream);
@ -620,18 +662,33 @@ const StreamsTable = ({ onReady }) => {
};
// Separate function to actually execute single channel creation
const executeSingleChannelCreation = async (stream, channelNumber = null) => {
const selectedChannelProfileId =
useChannelsStore.getState().selectedProfileId;
const executeSingleChannelCreation = async (
stream,
channelNumber = null,
profileIds = null
) => {
// Convert profile selection: 'all' means all profiles (null), 'none' means no profiles ([]), specific IDs otherwise
let channelProfileIds;
if (profileIds) {
if (profileIds.includes('none')) {
channelProfileIds = [];
} else if (profileIds.includes('all')) {
channelProfileIds = null;
} else {
channelProfileIds = profileIds
.filter((id) => id !== 'all' && id !== 'none')
.map((id) => parseInt(id));
}
} else {
channelProfileIds =
selectedProfileId !== '0' ? [parseInt(selectedProfileId)] : null;
}
await API.createChannelFromStream({
name: stream.name,
channel_number: channelNumber,
stream_id: stream.id,
// Only pass channel_profile_ids if a specific profile is selected (not "All")
...(selectedChannelProfileId !== '0' && {
channel_profile_ids: selectedChannelProfileId,
}),
channel_profile_ids: channelProfileIds,
});
await API.requeryChannels();
const fetchLogos = useChannelsStore.getState().fetchLogos;
@ -663,7 +720,8 @@ const StreamsTable = ({ onReady }) => {
setSingleChannelModalOpen(false);
await executeSingleChannelCreation(
currentStreamForChannel,
channelNumberValue
channelNumberValue,
singleSelectedProfileIds
);
};
@ -1134,145 +1192,41 @@ const StreamsTable = ({ onReady }) => {
onClose={closeStreamForm}
/>
{/* Channel Numbering Modal */}
<Modal
{/* Bulk Channel Creation Modal */}
<CreateChannelModal
opened={channelNumberingModalOpen}
onClose={() => setChannelNumberingModalOpen(false)}
title="Channel Numbering Options"
size="md"
centered
>
<Stack spacing="md">
<Text size="sm" c="dimmed">
Choose how to assign channel numbers to the{' '}
{selectedStreamIds.length} selected streams:
</Text>
mode={numberingMode}
onModeChange={setNumberingMode}
numberValue={customStartNumber}
onNumberValueChange={setCustomStartNumber}
rememberChoice={rememberChoice}
onRememberChoiceChange={setRememberChoice}
onConfirm={handleChannelNumberingConfirm}
isBulk={true}
streamCount={selectedStreamIds.length}
selectedProfileIds={bulkSelectedProfileIds}
onProfileIdsChange={setBulkSelectedProfileIds}
channelProfiles={channelProfiles ? Object.values(channelProfiles) : []}
/>
<Radio.Group
value={numberingMode}
onChange={setNumberingMode}
label="Numbering Mode"
>
<Stack mt="xs" spacing="xs">
<Radio
value="provider"
label="Use Provider Numbers"
description="Use tvg-chno or channel-number from stream metadata, auto-assign for conflicts"
/>
<Radio
value="auto"
label="Auto-Assign Sequential"
description="Start from the lowest available channel number and increment by 1"
/>
<Radio
value="custom"
label="Start from Custom Number"
description="Start sequential numbering from a specific channel number"
/>
</Stack>
</Radio.Group>
{numberingMode === 'custom' && (
<NumberInput
label="Starting Channel Number"
description="Channel numbers will be assigned starting from this number"
value={customStartNumber}
onChange={setCustomStartNumber}
min={1}
placeholder="Enter starting number..."
/>
)}
<Checkbox
checked={rememberChoice}
onChange={(event) => setRememberChoice(event.currentTarget.checked)}
label="Remember this choice and don't ask again"
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => setChannelNumberingModalOpen(false)}
>
Cancel
</Button>
<Button onClick={handleChannelNumberingConfirm}>
Create Channels
</Button>
</Group>
</Stack>
</Modal>
{/* Single Channel Numbering Modal */}
<Modal
{/* Single Channel Creation Modal */}
<CreateChannelModal
opened={singleChannelModalOpen}
onClose={() => setSingleChannelModalOpen(false)}
title="Channel Number Assignment"
size="md"
centered
>
<Stack spacing="md">
<Text size="sm" c="dimmed">
Choose how to assign the channel number for "
{currentStreamForChannel?.name}":
</Text>
<Radio.Group
value={singleChannelMode}
onChange={setSingleChannelMode}
label="Number Assignment"
>
<Stack mt="xs" spacing="xs">
<Radio
value="provider"
label="Use Provider Number"
description="Use tvg-chno or channel-number from stream metadata, auto-assign if not available"
/>
<Radio
value="auto"
label="Auto-Assign Next Available"
description="Automatically assign the next available channel number"
/>
<Radio
value="specific"
label="Use Specific Number"
description="Use a specific channel number"
/>
</Stack>
</Radio.Group>
{singleChannelMode === 'specific' && (
<NumberInput
label="Channel Number"
description="The specific channel number to assign"
value={specificChannelNumber}
onChange={setSpecificChannelNumber}
min={1}
placeholder="Enter channel number..."
/>
)}
<Checkbox
checked={rememberSingleChoice}
onChange={(event) =>
setRememberSingleChoice(event.currentTarget.checked)
}
label="Remember this choice and don't ask again"
/>
<Group justify="flex-end" mt="md">
<Button
variant="default"
onClick={() => setSingleChannelModalOpen(false)}
>
Cancel
</Button>
<Button onClick={handleSingleChannelNumberingConfirm}>
Create Channel
</Button>
</Group>
</Stack>
</Modal>
mode={singleChannelMode}
onModeChange={setSingleChannelMode}
numberValue={specificChannelNumber}
onNumberValueChange={setSpecificChannelNumber}
rememberChoice={rememberSingleChoice}
onRememberChoiceChange={setRememberSingleChoice}
onConfirm={handleSingleChannelNumberingConfirm}
isBulk={false}
streamName={currentStreamForChannel?.name}
selectedProfileIds={singleSelectedProfileIds}
onProfileIdsChange={setSingleSelectedProfileIds}
channelProfiles={channelProfiles ? Object.values(channelProfiles) : []}
/>
<ConfirmationDialog
opened={confirmDeleteOpen}