Disable buttons that can't be used.

This commit is contained in:
SergeantPanda 2025-07-12 16:57:05 -05:00
parent 9cb05a0ae1
commit adc6604fa2
9 changed files with 120 additions and 37 deletions

View file

@ -187,6 +187,40 @@ class ChannelGroupViewSet(viewsets.ModelViewSet):
except KeyError:
return [Authenticated()]
def get_queryset(self):
"""Add annotation for association counts"""
from django.db.models import Count
return ChannelGroup.objects.annotate(
channel_count=Count('channels', distinct=True),
m3u_account_count=Count('m3u_account', distinct=True)
)
def update(self, request, *args, **kwargs):
"""Override update to check M3U associations"""
instance = self.get_object()
# Check if group has M3U account associations
if hasattr(instance, 'm3u_account') and instance.m3u_account.exists():
return Response(
{"error": "Cannot edit group with M3U account associations"},
status=status.HTTP_400_BAD_REQUEST
)
return super().update(request, *args, **kwargs)
def partial_update(self, request, *args, **kwargs):
"""Override partial_update to check M3U associations"""
instance = self.get_object()
# Check if group has M3U account associations
if hasattr(instance, 'm3u_account') and instance.m3u_account.exists():
return Response(
{"error": "Cannot edit group with M3U account associations"},
status=status.HTTP_400_BAD_REQUEST
)
return super().partial_update(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs):
"""Override destroy to check for associations before deletion"""
instance = self.get_object()

View file

@ -89,9 +89,12 @@ class StreamSerializer(serializers.ModelSerializer):
# Channel Group
#
class ChannelGroupSerializer(serializers.ModelSerializer):
channel_count = serializers.IntegerField(read_only=True)
m3u_account_count = serializers.IntegerField(read_only=True)
class Meta:
model = ChannelGroup
fields = ["id", "name"]
fields = ["id", "name", "channel_count", "m3u_account_count"]
class ChannelProfileSerializer(serializers.ModelSerializer):

View file

@ -250,7 +250,15 @@ export default class API {
});
if (response.id) {
useChannelsStore.getState().addChannelGroup(response);
// Add association flags for new groups
const processedGroup = {
...response,
hasChannels: false,
hasM3UAccounts: false,
canEdit: true,
canDelete: true
};
useChannelsStore.getState().addChannelGroup(processedGroup);
}
return response;

View file

@ -45,6 +45,8 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
const groupListRef = useRef(null);
const channelGroups = useChannelsStore((s) => s.channelGroups);
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
const logos = useChannelsStore((s) => s.logos);
const fetchLogos = useChannelsStore((s) => s.fetchLogos);
const streams = useStreamsStore((state) => state.streams);

View file

@ -32,6 +32,8 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => {
const groupListRef = useRef(null);
const channelGroups = useChannelsStore((s) => s.channelGroups);
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
const streamProfiles = useStreamProfilesStore((s) => s.profiles);
const [channelGroupModelOpen, setChannelGroupModalOpen] = useState(false);

View file

@ -1,10 +1,17 @@
// Modal.js
import React from 'react';
import API from '../../api';
import { Flex, TextInput, Button, Modal } from '@mantine/core';
import { Flex, TextInput, Button, Modal, Alert } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { isNotEmpty, useForm } from '@mantine/form';
import useChannelsStore from '../../store/channels';
const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
// Check if editing is allowed
const canEdit = !channelGroup || canEditChannelGroup(channelGroup.id);
const form = useForm({
mode: 'uncontrolled',
initialValues: {
@ -17,6 +24,16 @@ const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
});
const onSubmit = async () => {
// Prevent submission if editing is not allowed
if (channelGroup && !canEdit) {
notifications.show({
title: 'Error',
message: 'Cannot edit group with M3U account associations',
color: 'red',
});
return;
}
const values = form.getValues();
let newGroup;
@ -36,11 +53,17 @@ const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
return (
<Modal opened={isOpen} onClose={onClose} title="Channel Group">
{channelGroup && !canEdit && (
<Alert color="yellow" mb="md">
This group cannot be edited because it has M3U account associations.
</Alert>
)}
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
id="name"
name="name"
label="Name"
disabled={channelGroup && !canEdit}
{...form.getInputProps('name')}
key={form.key('name')}
/>
@ -50,7 +73,7 @@ const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
type="submit"
variant="contained"
color="primary"
disabled={form.submitting}
disabled={form.submitting || (channelGroup && !canEdit)}
size="small"
>
Submit

View file

@ -37,7 +37,9 @@ const GroupItem = React.memo(({
onCancelEdit,
onEdit,
onDelete,
groupUsage
groupUsage,
canEditGroup,
canDeleteGroup
}) => {
const getGroupBadges = (group) => {
const usage = groupUsage[group.id];
@ -62,16 +64,6 @@ const GroupItem = React.memo(({
return badges;
};
const canEditGroup = (group) => {
const usage = groupUsage[group.id];
return usage?.canEdit !== false;
};
const canDeleteGroup = (group) => {
const usage = groupUsage[group.id];
return usage?.canDelete !== false && !usage?.hasChannels && !usage?.hasM3UAccounts;
};
return (
<Group justify="space-between" p="sm" style={{
border: '1px solid #e0e0e0',
@ -133,9 +125,9 @@ const GroupItem = React.memo(({
});
const GroupManager = React.memo(({ isOpen, onClose }) => {
// Use a more specific selector to avoid unnecessary re-renders
const fetchChannelGroups = useChannelsStore((s) => s.fetchChannelGroups);
const channelGroups = useChannelsStore((s) => s.channelGroups);
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
const canDeleteChannelGroup = useChannelsStore((s) => s.canDeleteChannelGroup);
const [editingGroup, setEditingGroup] = useState(null);
const [editName, setEditName] = useState('');
const [newGroupName, setNewGroupName] = useState('');
@ -171,21 +163,18 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
}
}, [isOpen]);
const fetchGroupUsage = async () => {
const fetchGroupUsage = useCallback(async () => {
setLoading(true);
try {
// This would ideally be a dedicated API endpoint, but we'll use the existing data
// For now, we'll determine usage based on the group having associated data
// Use the actual channel group data that already has the flags
const usage = {};
// Check which groups have channels or M3U associations
// This is a simplified check - in a real implementation you'd want a dedicated API
Object.values(channelGroups).forEach(group => {
usage[group.id] = {
hasChannels: false, // Would need API call to check
hasM3UAccounts: false, // Would need API call to check
canEdit: true, // Assume editable unless proven otherwise
canDelete: true // Assume deletable unless proven otherwise
hasChannels: group.hasChannels ?? false,
hasM3UAccounts: group.hasM3UAccounts ?? false,
canEdit: group.canEdit ?? true,
canDelete: group.canDelete ?? true
};
});
@ -195,7 +184,7 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
} finally {
setLoading(false);
}
};
}, [channelGroups]);
const handleEdit = useCallback((group) => {
setEditingGroup(group.id);
@ -414,6 +403,8 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
onEdit={handleEdit}
onDelete={handleDelete}
groupUsage={groupUsage}
canEditGroup={canEditChannelGroup}
canDeleteGroup={canDeleteChannelGroup}
/>
))}
</Stack>
@ -431,5 +422,4 @@ const GroupManager = React.memo(({ isOpen, onClose }) => {
</Modal>
);
});
export default GroupManager;

View file

@ -216,6 +216,9 @@ const ChannelRowActions = React.memo(
const ChannelsTable = ({ }) => {
const theme = useMantineTheme();
const channelGroups = useChannelsStore((s) => s.channelGroups);
const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup);
const canDeleteChannelGroup = useChannelsStore((s) => s.canDeleteChannelGroup);
/**
* STORES
@ -241,7 +244,6 @@ const ChannelsTable = ({ }) => {
const channels = useChannelsStore((s) => s.channels);
const profiles = useChannelsStore((s) => s.profiles);
const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
const channelGroups = useChannelsStore((s) => s.channelGroups);
const logos = useChannelsStore((s) => s.logos);
const [tablePrefs, setTablePrefs] = useLocalStorage('channel-table-prefs', {
pageSize: 50,

View file

@ -46,16 +46,24 @@ const useChannelsStore = create((set, get) => ({
},
fetchChannelGroups: async () => {
set({ isLoading: true, error: null });
try {
const channelGroups = await api.getChannelGroups();
set({
channelGroups: channelGroups.reduce((acc, group) => {
acc[group.id] = group;
return acc;
}, {}),
isLoading: false,
});
// Process groups to add association flags
const processedGroups = channelGroups.reduce((acc, group) => {
acc[group.id] = {
...group,
hasChannels: group.channel_count > 0,
hasM3UAccounts: group.m3u_account_count > 0,
canEdit: group.m3u_account_count === 0,
canDelete: group.channel_count === 0 && group.m3u_account_count === 0
};
return acc;
}, {});
set((state) => ({
channelGroups: processedGroups,
}));
} catch (error) {
console.error('Failed to fetch channel groups:', error);
set({ error: 'Failed to load channel groups.', isLoading: false });
@ -435,6 +443,17 @@ const useChannelsStore = create((set, get) => ({
set({ error: 'Failed to load recordings.', isLoading: false });
}
},
// Add helper methods for validation
canEditChannelGroup: (groupIdOrGroup) => {
const groupId = typeof groupIdOrGroup === 'object' ? groupIdOrGroup.id : groupIdOrGroup;
return get().channelGroups[groupId]?.canEdit ?? true;
},
canDeleteChannelGroup: (groupIdOrGroup) => {
const groupId = typeof groupIdOrGroup === 'object' ? groupIdOrGroup.id : groupIdOrGroup;
return get().channelGroups[groupId]?.canDelete ?? true;
},
}));
export default useChannelsStore;