diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 23d706ba..23632704 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -185,6 +185,30 @@ class ChannelViewSet(viewsets.ModelViewSet): context['include_streams'] = include_streams return context + @action(detail=False, methods=['patch'], url_path='edit/bulk') + def edit_bulk(self, request): + data_list = request.data + if not isinstance(data_list, list): + return Response({"error": "Expected a list of channel objects objects"}, status=status.HTTP_400_BAD_REQUEST) + + updated_channels = [] + try: + with transaction.atomic(): + for item in data_list: + channel = Channel.objects.id(id=item.pop('id')) + for key, value in item.items(): + setattr(channel, key, value) + + channel.save(update_fields=item.keys()) + updated_channels.append(channel) + except Exception as e: + logger.error("Error during bulk channel edit", e) + return Response({"error": e}, status=500) + + response_data = ChannelSerializer(updated_channels, many=True).data + + return Response(response_data, status=status.HTTP_200_OK) + @action(detail=False, methods=['get'], url_path='ids') def get_ids(self, request, *args, **kwargs): # Get the filtered queryset @@ -204,12 +228,13 @@ class ChannelViewSet(viewsets.ModelViewSet): operation_description="Auto-assign channel_number in bulk by an ordered list of channel IDs.", request_body=openapi.Schema( type=openapi.TYPE_OBJECT, - required=["channel_order"], + required=["channel_ids"], properties={ - "channel_order": openapi.Schema( + "starting_number": openapi.Schema(type=openapi.TYPE_STRING, description="Starting channel number to assign"), + "channel_ids": openapi.Schema( type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_INTEGER), - description="List of channel IDs in the new order" + description="Channel IDs to assign" ) } ), @@ -218,9 +243,11 @@ class ChannelViewSet(viewsets.ModelViewSet): @action(detail=False, methods=['post'], url_path='assign') def assign(self, request): with transaction.atomic(): - channel_order = request.data.get('channel_order', []) - for order, channel_id in enumerate(channel_order, start=1): - Channel.objects.filter(id=channel_id).update(channel_number=order) + channel_ids = request.data.get('channel_ids', []) + channel_num = request.data.get('starting_number', 1) + for channel_id in channel_ids: + Channel.objects.filter(id=channel_id).update(channel_number=channel_num) + channel_num = channel_num + 1 return Response({"message": "Channels have been auto-assigned!"}, status=status.HTTP_200_OK) diff --git a/frontend/src/api.js b/frontend/src/api.js index a4fb89b0..60b22634 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -378,6 +378,31 @@ export default class API { } } + static async updateChannels(ids, values) { + const body = []; + for (const id in ids) { + body.push({ + id, + ...values, + }); + } + + try { + const response = await request( + `${host}/api/channels/channels/edit/bulk/`, + { + method: 'PATCH', + body, + } + ); + + useChannelsStore.getState().updateChannels(response); + return response; + } catch (e) { + errorNotification('Failed to update channels', e); + } + } + static async setChannelEPG(channelId, epgDataId) { try { const response = await request( @@ -408,16 +433,13 @@ export default class API { } } - static async assignChannelNumbers(channelIds) { + static async assignChannelNumbers(channelIds, startingNum = 1) { try { const response = await request(`${host}/api/channels/channels/assign/`, { method: 'POST', - body: { channel_order: channelIds }, + body: { channel_ids: channelIds, starting_number: startingNum }, }); - // Optionally refesh the channel list in Zustand - // await useChannelsStore.getState().fetchChannels(); - return response; } catch (e) { errorNotification('Failed to assign channel #s', e); @@ -733,7 +755,11 @@ export default class API { try { // If this is just toggling the active state, make a simpler request - if (isToggle && 'is_active' in payload && Object.keys(payload).length === 1) { + if ( + isToggle && + 'is_active' in payload && + Object.keys(payload).length === 1 + ) { const response = await request(`${host}/api/m3u/accounts/${id}/`, { method: 'PATCH', body: { is_active: payload.is_active }, @@ -830,7 +856,11 @@ export default class API { try { // If this is just toggling the active state, make a simpler request - if (isToggle && 'is_active' in payload && Object.keys(payload).length === 1) { + if ( + isToggle && + 'is_active' in payload && + Object.keys(payload).length === 1 + ) { const response = await request(`${host}/api/epg/sources/${id}/`, { method: 'PATCH', body: { is_active: payload.is_active }, @@ -1344,7 +1374,9 @@ export default class API { static async getChannel(id) { try { - const response = await request(`${host}/api/channels/channels/${id}/?include_streams=true`); + const response = await request( + `${host}/api/channels/channels/${id}/?include_streams=true` + ); return response; } catch (e) { errorNotification('Failed to fetch channel details', e); diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 15b377f0..c8f92fc1 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -57,7 +57,7 @@ const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/ const hdhrUrlBase = `${window.location.protocol}//${window.location.host}/hdhr`; const ChannelEnabledSwitch = React.memo( - ({ rowId, selectedProfileId, toggleChannelEnabled }) => { + ({ rowId, selectedProfileId, selectedTableIds }) => { // Directly extract the channels set once to avoid re-renders on every change. const isEnabled = useChannelsStore( useCallback( @@ -68,9 +68,17 @@ const ChannelEnabledSwitch = React.memo( ) ); - const handleToggle = useCallback(() => { - toggleChannelEnabled([rowId], !isEnabled); - }, [rowId, isEnabled, toggleChannelEnabled]); + const handleToggle = () => { + if (selectedTableIds.length > 1) { + API.updateProfileChannels( + selectedTableIds, + selectedProfileId, + !isEnabled + ); + } else { + API.updateProfileChannel(rowId, selectedProfileId, !isEnabled); + } + }; return (