mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Reset pagination to page 1 when toggling "Only Empty Channels" filter to prevent requesting non-existent pages after filtering reduces results. Fixes #864
439 lines
12 KiB
JavaScript
439 lines
12 KiB
JavaScript
import React, { useState } from 'react';
|
|
import {
|
|
ActionIcon,
|
|
Box,
|
|
Button,
|
|
Flex,
|
|
Group,
|
|
Menu,
|
|
NumberInput,
|
|
Popover,
|
|
Select,
|
|
Text,
|
|
TextInput,
|
|
Tooltip,
|
|
useMantineTheme,
|
|
} from '@mantine/core';
|
|
import {
|
|
ArrowDown01,
|
|
Binary,
|
|
CircleCheck,
|
|
EllipsisVertical,
|
|
SquareMinus,
|
|
SquarePen,
|
|
SquarePlus,
|
|
Settings,
|
|
Eye,
|
|
EyeOff,
|
|
Filter,
|
|
Square,
|
|
SquareCheck,
|
|
} from 'lucide-react';
|
|
import API from '../../../api';
|
|
import { notifications } from '@mantine/notifications';
|
|
import useChannelsStore from '../../../store/channels';
|
|
import useAuthStore from '../../../store/auth';
|
|
import { USER_LEVELS } from '../../../constants';
|
|
import AssignChannelNumbersForm from '../../forms/AssignChannelNumbers';
|
|
import GroupManager from '../../forms/GroupManager';
|
|
import ConfirmationDialog from '../../ConfirmationDialog';
|
|
import useWarningsStore from '../../../store/warnings';
|
|
import ProfileModal, { renderProfileOption } from '../../modals/ProfileModal';
|
|
|
|
const CreateProfilePopover = React.memo(() => {
|
|
const [opened, setOpened] = useState(false);
|
|
const [name, setName] = useState('');
|
|
const theme = useMantineTheme();
|
|
|
|
const authUser = useAuthStore((s) => s.user);
|
|
|
|
const setOpen = () => {
|
|
setName('');
|
|
setOpened(!opened);
|
|
};
|
|
|
|
const submit = async () => {
|
|
await API.addChannelProfile({ name });
|
|
setName('');
|
|
setOpened(false);
|
|
};
|
|
|
|
return (
|
|
<Popover
|
|
opened={opened}
|
|
onChange={setOpen}
|
|
position="bottom"
|
|
withArrow
|
|
shadow="md"
|
|
>
|
|
<Popover.Target>
|
|
<ActionIcon
|
|
variant="transparent"
|
|
color={theme.tailwind.green[5]}
|
|
onClick={setOpen}
|
|
disabled={authUser.user_level != USER_LEVELS.ADMIN}
|
|
>
|
|
<SquarePlus />
|
|
</ActionIcon>
|
|
</Popover.Target>
|
|
|
|
<Popover.Dropdown>
|
|
<Group>
|
|
<TextInput
|
|
placeholder="Profile Name"
|
|
value={name}
|
|
onChange={(event) => setName(event.currentTarget.value)}
|
|
size="xs"
|
|
/>
|
|
|
|
<ActionIcon
|
|
variant="transparent"
|
|
color={theme.tailwind.green[5]}
|
|
size="sm"
|
|
onClick={submit}
|
|
>
|
|
<CircleCheck />
|
|
</ActionIcon>
|
|
</Group>
|
|
</Popover.Dropdown>
|
|
</Popover>
|
|
);
|
|
});
|
|
|
|
const ChannelTableHeader = ({
|
|
rows,
|
|
editChannel,
|
|
deleteChannels,
|
|
selectedTableIds,
|
|
showDisabled,
|
|
setShowDisabled,
|
|
showOnlyStreamlessChannels,
|
|
setShowOnlyStreamlessChannels,
|
|
pagination,
|
|
setPagination,
|
|
}) => {
|
|
const theme = useMantineTheme();
|
|
|
|
const [channelNumAssignmentStart, setChannelNumAssignmentStart] = useState(1);
|
|
const [assignNumbersModalOpen, setAssignNumbersModalOpen] = useState(false);
|
|
const [groupManagerOpen, setGroupManagerOpen] = useState(false);
|
|
const [confirmDeleteProfileOpen, setConfirmDeleteProfileOpen] =
|
|
useState(false);
|
|
const [profileToDelete, setProfileToDelete] = useState(null);
|
|
const [deletingProfile, setDeletingProfile] = useState(false);
|
|
const [profileModalState, setProfileModalState] = useState({
|
|
opened: false,
|
|
mode: null,
|
|
profileId: null,
|
|
});
|
|
|
|
const profiles = useChannelsStore((s) => s.profiles);
|
|
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
|
|
const setSelectedProfileId = useChannelsStore((s) => s.setSelectedProfileId);
|
|
const authUser = useAuthStore((s) => s.user);
|
|
const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
|
|
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
|
const closeAssignChannelNumbersModal = () => {
|
|
setAssignNumbersModalOpen(false);
|
|
};
|
|
|
|
const closeProfileModal = () => {
|
|
setProfileModalState({ opened: false, mode: null, profileId: null });
|
|
};
|
|
|
|
const openProfileModal = (mode, profileId) => {
|
|
if (!profiles[profileId]) return;
|
|
setProfileModalState({ opened: true, mode, profileId });
|
|
};
|
|
|
|
const deleteProfile = async (id) => {
|
|
// Get profile details for the confirmation dialog
|
|
const profileObj = profiles[id];
|
|
setProfileToDelete(profileObj);
|
|
|
|
// Skip warning if it's been suppressed
|
|
if (isWarningSuppressed('delete-profile')) {
|
|
return executeDeleteProfile(id);
|
|
}
|
|
|
|
setConfirmDeleteProfileOpen(true);
|
|
};
|
|
|
|
const executeDeleteProfile = async (id) => {
|
|
setDeletingProfile(true);
|
|
try {
|
|
await API.deleteChannelProfile(id);
|
|
} finally {
|
|
setDeletingProfile(false);
|
|
setConfirmDeleteProfileOpen(false);
|
|
}
|
|
};
|
|
|
|
const matchEpg = async () => {
|
|
try {
|
|
// Hit our new endpoint that triggers the fuzzy matching Celery task
|
|
// If channels are selected, only match those; otherwise match all
|
|
if (selectedTableIds.length > 0) {
|
|
await API.matchEpg(selectedTableIds);
|
|
notifications.show({
|
|
title: `EPG matching task started for ${selectedTableIds.length} selected channel(s)!`,
|
|
});
|
|
} else {
|
|
await API.matchEpg();
|
|
notifications.show({
|
|
title: 'EPG matching task started for all channels without EPG!',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
notifications.show(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const assignChannels = async () => {
|
|
try {
|
|
// Call our custom API endpoint
|
|
const result = await API.assignChannelNumbers(
|
|
selectedTableIds,
|
|
channelNumAssignmentStart
|
|
);
|
|
|
|
// We might get { message: "Channels have been auto-assigned!" }
|
|
notifications.show({
|
|
title: result.message || 'Channels assigned',
|
|
color: 'green.5',
|
|
});
|
|
|
|
// Refresh the channel list
|
|
// await fetchChannels();
|
|
API.requeryChannels();
|
|
} catch (err) {
|
|
console.error(err);
|
|
notifications.show({
|
|
title: 'Failed to assign channels',
|
|
color: 'red.5',
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderModalOption = renderProfileOption(
|
|
theme,
|
|
profiles,
|
|
openProfileModal,
|
|
deleteProfile,
|
|
authUser
|
|
);
|
|
|
|
const toggleShowDisabled = () => {
|
|
setShowDisabled(!showDisabled);
|
|
};
|
|
|
|
const toggleShowOnlyStreamlessChannels = () => {
|
|
setPagination({ ...pagination, pageIndex: 0 });
|
|
setShowOnlyStreamlessChannels(!showOnlyStreamlessChannels);
|
|
};
|
|
|
|
return (
|
|
<Group justify="space-between">
|
|
<Group gap={5} style={{ paddingLeft: 10 }}>
|
|
<Select
|
|
size="xs"
|
|
allowDeselect={false}
|
|
value={selectedProfileId}
|
|
onChange={setSelectedProfileId}
|
|
data={Object.values(profiles).map((profile) => ({
|
|
label: profile.name,
|
|
value: `${profile.id}`,
|
|
}))}
|
|
renderOption={renderModalOption}
|
|
style={{ minWidth: 190 }}
|
|
/>
|
|
|
|
<Tooltip label="Create Profile">
|
|
<CreateProfilePopover />
|
|
</Tooltip>
|
|
</Group>
|
|
|
|
<Box
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'flex-end',
|
|
padding: 10,
|
|
}}
|
|
>
|
|
<Flex gap={6}>
|
|
<Menu shadow="md" width={200}>
|
|
<Menu.Target>
|
|
<Button size="xs" variant="default" onClick={() => {}}>
|
|
<Filter size={18} />
|
|
</Button>
|
|
</Menu.Target>
|
|
|
|
<Menu.Dropdown>
|
|
<Menu.Item
|
|
onClick={toggleShowDisabled}
|
|
leftSection={
|
|
showDisabled ? <Eye size={18} /> : <EyeOff size={18} />
|
|
}
|
|
disabled={selectedProfileId === '0'}
|
|
>
|
|
<Text size="xs">
|
|
{showDisabled ? 'Hide Disabled' : 'Show Disabled'}
|
|
</Text>
|
|
</Menu.Item>
|
|
|
|
<Menu.Item
|
|
onClick={toggleShowOnlyStreamlessChannels}
|
|
leftSection={
|
|
showOnlyStreamlessChannels ? (
|
|
<SquareCheck size={18} />
|
|
) : (
|
|
<Square size={18} />
|
|
)
|
|
}
|
|
>
|
|
<Text size="xs">Only Empty Channels</Text>
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
|
|
<Button
|
|
leftSection={<SquarePen size={18} />}
|
|
variant="default"
|
|
size="xs"
|
|
onClick={() => editChannel()}
|
|
disabled={
|
|
selectedTableIds.length == 0 ||
|
|
authUser.user_level != USER_LEVELS.ADMIN
|
|
}
|
|
>
|
|
Edit
|
|
</Button>
|
|
|
|
<Button
|
|
leftSection={<SquareMinus size={18} />}
|
|
variant="default"
|
|
size="xs"
|
|
onClick={deleteChannels}
|
|
disabled={
|
|
selectedTableIds.length == 0 ||
|
|
authUser.user_level != USER_LEVELS.ADMIN
|
|
}
|
|
>
|
|
Delete
|
|
</Button>
|
|
|
|
<Button
|
|
leftSection={<SquarePlus size={18} />}
|
|
variant="light"
|
|
size="xs"
|
|
onClick={() => editChannel(null, { forceAdd: true })}
|
|
disabled={authUser.user_level != USER_LEVELS.ADMIN}
|
|
p={5}
|
|
color={theme.tailwind.green[5]}
|
|
style={{
|
|
...(authUser.user_level == USER_LEVELS.ADMIN && {
|
|
borderWidth: '1px',
|
|
borderColor: theme.tailwind.green[5],
|
|
color: 'white',
|
|
}),
|
|
}}
|
|
>
|
|
Add
|
|
</Button>
|
|
|
|
<Menu>
|
|
<Menu.Target>
|
|
<ActionIcon variant="default" size={30}>
|
|
<EllipsisVertical size={18} />
|
|
</ActionIcon>
|
|
</Menu.Target>
|
|
|
|
<Menu.Dropdown>
|
|
<Menu.Item
|
|
leftSection={<ArrowDown01 size={18} />}
|
|
disabled={
|
|
selectedTableIds.length == 0 ||
|
|
authUser.user_level != USER_LEVELS.ADMIN
|
|
}
|
|
onClick={() => setAssignNumbersModalOpen(true)}
|
|
>
|
|
<Text size="xs">Assign #s</Text>
|
|
</Menu.Item>
|
|
|
|
<Menu.Item
|
|
leftSection={<Binary size={18} />}
|
|
disabled={authUser.user_level != USER_LEVELS.ADMIN}
|
|
onClick={matchEpg}
|
|
>
|
|
<Text size="xs">
|
|
{selectedTableIds.length > 0
|
|
? `Auto-Match (${selectedTableIds.length} selected)`
|
|
: 'Auto-Match EPG'}
|
|
</Text>
|
|
</Menu.Item>
|
|
|
|
<Menu.Item
|
|
leftSection={<Settings size={18} />}
|
|
disabled={authUser.user_level != USER_LEVELS.ADMIN}
|
|
onClick={() => setGroupManagerOpen(true)}
|
|
>
|
|
<Text size="xs">Edit Groups</Text>
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Flex>
|
|
</Box>
|
|
|
|
<ProfileModal
|
|
opened={profileModalState.opened}
|
|
onClose={closeProfileModal}
|
|
mode={profileModalState.mode}
|
|
profile={
|
|
profileModalState.profileId
|
|
? profiles[profileModalState.profileId]
|
|
: null
|
|
}
|
|
onDeleteProfile={deleteProfile}
|
|
/>
|
|
|
|
<AssignChannelNumbersForm
|
|
channelIds={selectedTableIds}
|
|
isOpen={assignNumbersModalOpen}
|
|
onClose={closeAssignChannelNumbersModal}
|
|
/>
|
|
|
|
<GroupManager
|
|
isOpen={groupManagerOpen}
|
|
onClose={() => setGroupManagerOpen(false)}
|
|
/>
|
|
|
|
<ConfirmationDialog
|
|
opened={confirmDeleteProfileOpen}
|
|
onClose={() => setConfirmDeleteProfileOpen(false)}
|
|
onConfirm={() => executeDeleteProfile(profileToDelete?.id)}
|
|
loading={deletingProfile}
|
|
title="Confirm Profile Deletion"
|
|
message={
|
|
profileToDelete ? (
|
|
<div style={{ whiteSpace: 'pre-line' }}>
|
|
{`Are you sure you want to delete the following profile?
|
|
|
|
Name: ${profileToDelete.name}
|
|
|
|
This action cannot be undone.`}
|
|
</div>
|
|
) : (
|
|
'Are you sure you want to delete this profile? This action cannot be undone.'
|
|
)
|
|
}
|
|
confirmLabel="Delete"
|
|
cancelLabel="Cancel"
|
|
actionKey="delete-profile"
|
|
onSuppressChange={suppressWarning}
|
|
size="md"
|
|
/>
|
|
</Group>
|
|
);
|
|
};
|
|
|
|
export default ChannelTableHeader;
|