diff --git a/frontend/src/components/forms/LiveGroupFilter.jsx b/frontend/src/components/forms/LiveGroupFilter.jsx index 06187bc0..e4580fc2 100644 --- a/frontend/src/components/forms/LiveGroupFilter.jsx +++ b/frontend/src/components/forms/LiveGroupFilter.jsx @@ -95,8 +95,8 @@ const LiveGroupFilter = ({ ? groupStates.find((g) => g.channel_group === configuringGroupId) : null; const applyGroupChange = (nextGroupState) => { - setGroupStates( - groupStates.map((state) => + setGroupStates((prev) => + prev.map((state) => state.channel_group === nextGroupState.channel_group ? nextGroupState : state @@ -497,8 +497,8 @@ const LiveGroupFilter = ({ }, [playlist, channelGroups]); const toggleGroupEnabled = (id) => { - setGroupStates( - groupStates.map((state) => ({ + setGroupStates((prev) => + prev.map((state) => ({ ...state, enabled: state.channel_group == id ? !state.enabled : state.enabled, })) @@ -506,8 +506,8 @@ const LiveGroupFilter = ({ }; const toggleAutoSync = (id) => { - setGroupStates( - groupStates.map((state) => { + setGroupStates((prev) => + prev.map((state) => { if (state.channel_group != id) return state; const turningOn = !state.auto_channel_sync; const next = { ...state, auto_channel_sync: turningOn }; @@ -520,7 +520,7 @@ const LiveGroupFilter = ({ if (currentStart && currentStart > 1) return next; let proposedStart = 1; - for (const other of groupStates) { + for (const other of prev) { if (other.channel_group == id) continue; if (!other.enabled || !other.auto_channel_sync) continue; const otherMode = @@ -550,8 +550,8 @@ const LiveGroupFilter = ({ // Handle logo selection from LogoForm const handleLogoSuccess = ({ logo }) => { if (logo && logo.id && currentEditingGroupId !== null) { - setGroupStates( - groupStates.map((state) => { + setGroupStates((prev) => + prev.map((state) => { if (state.channel_group === currentEditingGroupId) { return { ...state, @@ -564,7 +564,7 @@ const LiveGroupFilter = ({ return state; }) ); - ensureLogosLoaded(); // Refresh logos + ensureLogosLoaded(); } setLogoModalOpen(false); setCurrentEditingGroupId(null); @@ -582,8 +582,8 @@ const LiveGroupFilter = ({ }; const selectAll = () => { - setGroupStates( - groupStates.map((state) => ({ + setGroupStates((prev) => + prev.map((state) => ({ ...state, enabled: isVisible(state) ? true : state.enabled, })) @@ -591,8 +591,8 @@ const LiveGroupFilter = ({ }; const deselectAll = () => { - setGroupStates( - groupStates.map((state) => ({ + setGroupStates((prev) => + prev.map((state) => ({ ...state, enabled: isVisible(state) ? false : state.enabled, })) @@ -1803,8 +1803,8 @@ const LiveGroupFilter = ({ 'fixed' } onChange={(value) => { - setGroupStates( - groupStates.map((state) => { + setGroupStates((prev) => + prev.map((state) => { if ( state.channel_group === group.channel_group ) { diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index b0b08be0..e97c1caa 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -244,11 +244,10 @@ const ChannelRowActions = React.memo( ); }, - // Custom comparator: only re-render when the actual channel changes. - // The row object is a new TanStack Table reference on each render, but - // row.original.id is stable. Callbacks read fresh data at call time. - (prevProps, nextProps) => - prevProps.row.original.id === nextProps.row.original.id + // Custom comparator: skip re-render when the channel's data object hasn't + // changed. row.original is stable when the underlying channel hasn't been + // updated; it becomes a new reference when the store replaces that channel. + (prevProps, nextProps) => prevProps.row.original === nextProps.row.original ); const ChannelsTable = ({ onReady }) => { @@ -575,36 +574,30 @@ const ChannelsTable = ({ onReady }) => { })); }; - const editChannel = async (ch = null, opts = {}) => { - // If forceAdd is set, always open a blank form + const editChannel = useCallback(async (ch = null, opts = {}) => { if (opts.forceAdd) { setChannel(null); setChannelModalOpen(true); return; } - // Use table's selected state instead of store state to avoid stale selections - const currentSelection = table ? table.selectedTableIds : []; - console.log('editChannel called with:', { - ch, - currentSelection, - tableExists: !!table, - }); + const currentSelection = + useChannelsTableStore.getState().selectedChannelIds; + console.log('editChannel called with:', { ch, currentSelection }); if (currentSelection.length > 1) { setChannelBatchModalOpen(true); } else { - // If no channel object is passed but we have a selection, get the selected channel let channelToEdit = ch; if (!channelToEdit && currentSelection.length === 1) { const selectedId = currentSelection[0]; - - // Use table data since that's what's currently displayed - channelToEdit = data.find((d) => d.id === selectedId); + channelToEdit = useChannelsTableStore + .getState() + .channels.find((d) => d.id === selectedId); } setChannel(channelToEdit); setChannelModalOpen(true); } - }; + }, []); const deleteChannel = async (id) => { console.log(`Deleting channel with ID: ${id}`); @@ -1057,7 +1050,7 @@ const ChannelsTable = ({ onReady }) => { // from the store, so we don't need to recreate columns when logos load. // Note: tvgsLoaded is intentionally excluded - EditableEPGCell handles loading state internally // eslint-disable-next-line react-hooks/exhaustive-deps - [selectedProfileId, channelGroups, theme, tvgsById, epgs] + [selectedProfileId, channelGroups, theme, tvgsById, epgs, editChannel] ); const renderHeaderCell = (header) => {