From e2a915f10ba34a66c6ddb36bdffa71a41c33733b Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 26 Jan 2026 16:19:07 -0600 Subject: [PATCH] Perf: lazy load editable cells on focus, not on unlock. Also don't wait for all channels to load before logging in. --- .../src/components/tables/ChannelsTable.jsx | 20 +- .../tables/ChannelsTable/EditableCell.jsx | 561 ++++++++++-------- frontend/src/store/auth.jsx | 10 +- 3 files changed, 320 insertions(+), 271 deletions(-) diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 3d27ca61..f41ca72d 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -253,7 +253,7 @@ const ChannelsTable = ({ onReady }) => { const tvgsLoaded = useEPGsStore((s) => s.tvgsLoaded); // Get channel logos for logo selection - const { logos: channelLogos, ensureLogosLoaded } = useChannelLogoSelection(); + const { ensureLogosLoaded } = useChannelLogoSelection(); const theme = useMantineTheme(); const channelGroups = useChannelsStore((s) => s.channelGroups); @@ -977,19 +977,11 @@ const ChannelsTable = ({ onReady }) => { enableResizing: false, header: '', cell: (props) => ( - { - // Ensure logos are loaded when user tries to edit - ensureLogosLoaded(); - }} - style={{ width: '100%', height: '100%' }} - > - - + ), }, { diff --git a/frontend/src/components/tables/ChannelsTable/EditableCell.jsx b/frontend/src/components/tables/ChannelsTable/EditableCell.jsx index 065e4631..42d99e73 100644 --- a/frontend/src/components/tables/ChannelsTable/EditableCell.jsx +++ b/frontend/src/components/tables/ChannelsTable/EditableCell.jsx @@ -4,6 +4,7 @@ import React, { useEffect, useRef, useMemo, + memo, } from 'react'; import { Box, @@ -16,33 +17,88 @@ import { } from '@mantine/core'; import API from '../../../api'; import useChannelsTableStore from '../../../store/channelsTable'; +import useLogosStore from '../../../store/logos'; + +// Lightweight wrapper that only renders full editable cell when unlocked +// This prevents 250+ heavy component instances when table is locked +const EditableCellWrapper = memo( + ({ children, getValue, isUnlocked, renderLocked }) => { + if (!isUnlocked) { + // Render lightweight locked view + return renderLocked ? ( + renderLocked(getValue()) + ) : ( + + {getValue() ?? ''} + + ); + } + // Only render heavy component when unlocked + return children; + } +); // Editable text cell export const EditableTextCell = ({ row, column, getValue }) => { const isUnlocked = useChannelsTableStore((s) => s.isUnlocked); + const [isFocused, setIsFocused] = useState(false); + + // When locked or not focused, show simple display + if (!isUnlocked || !isFocused) { + return ( + isUnlocked && setIsFocused(true)} + style={{ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + cursor: isUnlocked ? 'text' : 'default', + padding: '0 4px', + }} + > + {getValue() ?? ''} + + ); + } + + // Only mount heavy component when actually editing + return ( + setIsFocused(false)} + /> + ); +}; + +// Inner component with all the editing logic - only rendered when focused +const EditableTextCellInner = ({ row, column, getValue, onBlur }) => { const initialValue = getValue() || ''; const [value, setValue] = useState(initialValue); - const [isFocused, setIsFocused] = useState(false); const previousValue = useRef(initialValue); const isMounted = useRef(false); const debounceTimer = useRef(null); useEffect(() => { const currentValue = getValue() || ''; - if (!isFocused && currentValue !== previousValue.current) { + if (currentValue !== previousValue.current) { setValue(currentValue); previousValue.current = currentValue; } - }, [getValue, isFocused]); + }, [getValue]); const saveValue = useCallback( async (newValue) => { - // Don't save if not mounted, not unlocked, or value hasn't changed - if ( - !isMounted.current || - !isUnlocked || - newValue === previousValue.current - ) { + // Don't save if not mounted or value hasn't changed + if (!isMounted.current || newValue === previousValue.current) { return; } @@ -62,7 +118,7 @@ export const EditableTextCell = ({ row, column, getValue }) => { setValue(previousValue.current || ''); } }, - [row.original.id, column.id, isUnlocked] + [row.original.id, column.id] ); useEffect(() => { @@ -77,7 +133,6 @@ export const EditableTextCell = ({ row, column, getValue }) => { }, []); const handleChange = (e) => { - if (!isUnlocked) return; const newValue = e.currentTarget.value; setValue(newValue); @@ -93,40 +148,10 @@ export const EditableTextCell = ({ row, column, getValue }) => { }; const handleBlur = () => { - setIsFocused(false); - if (isUnlocked) { - saveValue(value); - } + saveValue(value); + onBlur(); }; - const handleClick = () => { - if (isUnlocked) { - setIsFocused(true); - } - }; - - if (!isUnlocked || !isFocused) { - return ( - - {value} - - ); - } - return ( { // Editable number cell export const EditableNumberCell = ({ row, column, getValue }) => { const isUnlocked = useChannelsTableStore((s) => s.isUnlocked); + const [isFocused, setIsFocused] = useState(false); + + const value = getValue(); + const formattedValue = + value !== null && value !== undefined + ? value === Math.floor(value) + ? Math.floor(value) + : value + : ''; + + // When locked or not focused, show simple display + if (!isUnlocked || !isFocused) { + return ( + isUnlocked && setIsFocused(true)} + style={{ + textAlign: 'right', + width: '100%', + cursor: isUnlocked ? 'text' : 'default', + padding: '0 4px', + }} + > + {formattedValue} + + ); + } + + return ( + setIsFocused(false)} + /> + ); +}; + +// Inner component with all the editing logic - only rendered when focused +const EditableNumberCellInner = ({ row, column, getValue, onBlur }) => { const initialValue = getValue(); const [value, setValue] = useState(initialValue); - const [isFocused, setIsFocused] = useState(false); const previousValue = useRef(initialValue); const isMounted = useRef(false); useEffect(() => { const currentValue = getValue(); - if (!isFocused && currentValue !== previousValue.current) { + if (currentValue !== previousValue.current) { setValue(currentValue); previousValue.current = currentValue; } - }, [getValue, isFocused]); + }, [getValue]); const saveValue = useCallback( async (newValue) => { - // Don't save if not mounted, not unlocked, or value hasn't changed - if ( - !isMounted.current || - !isUnlocked || - newValue === previousValue.current - ) { + // Don't save if not mounted or value hasn't changed + if (!isMounted.current || newValue === previousValue.current) { return; } @@ -203,8 +262,7 @@ export const EditableNumberCell = ({ row, column, getValue }) => { // If channel_number was changed, refetch to reorder the table if (column.id === 'channel_number') { await API.requeryChannels(); - // Exit edit mode after resorting to avoid confusion - setIsFocused(false); + onBlur(); } } } catch (error) { @@ -212,7 +270,7 @@ export const EditableNumberCell = ({ row, column, getValue }) => { setValue(previousValue.current); } }, - [row.original.id, column.id, isUnlocked] + [row.original.id, column.id, onBlur] ); useEffect(() => { @@ -223,51 +281,14 @@ export const EditableNumberCell = ({ row, column, getValue }) => { }, []); const handleChange = (newValue) => { - if (!isUnlocked) return; setValue(newValue); }; const handleBlur = () => { - setIsFocused(false); - if (isUnlocked) { - saveValue(value); - } + saveValue(value); + onBlur(); }; - const handleClick = () => { - if (isUnlocked) { - setIsFocused(true); - } - }; - - const formattedValue = - value !== null && value !== undefined - ? value === Math.floor(value) - ? Math.floor(value) - : value - : ''; - - if (!isUnlocked || !isFocused) { - return ( - - {formattedValue} - - ); - } - return ( { }; // Editable select cell for groups -export const EditableGroupCell = ({ row, getValue, channelGroups }) => { +export const EditableGroupCell = ({ row, channelGroups }) => { const isUnlocked = useChannelsTableStore((s) => s.isUnlocked); + const [isFocused, setIsFocused] = useState(false); const groupId = row.original.channel_group_id; const groupName = channelGroups[groupId]?.name || ''; + + // Show simple display when locked OR when unlocked but not focused + if (!isUnlocked || !isFocused) { + return ( + isUnlocked && setIsFocused(true)} + style={{ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + padding: '0 4px', + cursor: isUnlocked ? 'pointer' : 'default', + }} + > + {groupName} + + ); + } + + return ( + setIsFocused(false)} + /> + ); +}; + +// Inner component with all the editing logic - only rendered when focused +const EditableGroupCellInner = ({ + row, + channelGroups, + groupName, + groupId, + onBlur, +}) => { const previousGroupId = useRef(groupId); - const [isFocused, setIsFocused] = useState(false); const [searchValue, setSearchValue] = useState(''); const saveValue = useCallback( async (newGroupId) => { - // Don't save if not unlocked or value hasn't changed - if ( - !isUnlocked || - String(newGroupId) === String(previousGroupId.current) - ) { + // Don't save if value hasn't changed + if (String(newGroupId) === String(previousGroupId.current)) { return; } @@ -324,18 +380,12 @@ export const EditableGroupCell = ({ row, getValue, channelGroups }) => { console.error('Failed to update channel group:', error); } }, - [row.original.id, isUnlocked] + [row.original.id] ); - const handleClick = () => { - if (isUnlocked) { - setIsFocused(true); - } - }; - const handleChange = (newGroupId) => { saveValue(newGroupId); - setIsFocused(false); + onBlur(); setSearchValue(''); }; @@ -344,33 +394,11 @@ export const EditableGroupCell = ({ row, getValue, channelGroups }) => { label: group.name, })); - if (!isUnlocked || !isFocused) { - return ( - - {groupName} - - ); - } - return ( setIsFocused(false)} + onBlur={onBlur} data={epgOptions} size="xs" variant="unstyled" @@ -599,17 +637,69 @@ export const EditableEPGCell = ({ }; // Editable cell for Logo selection -export const EditableLogoCell = ({ row, getValue, channelLogos, LazyLogo }) => { +export const EditableLogoCell = ({ + row, + getValue, + LazyLogo, + ensureLogosLoaded, +}) => { const isUnlocked = useChannelsTableStore((s) => s.isUnlocked); - const logoId = getValue(); - const previousLogoId = useRef(logoId); const [isFocused, setIsFocused] = useState(false); + const logoId = getValue(); + + const handleClick = () => { + if (isUnlocked) { + // Ensure logos are loaded when user tries to edit + ensureLogosLoaded?.(); + setIsFocused(true); + } + }; + + // Show simple display when locked OR when unlocked but not focused + if (!isUnlocked || !isFocused) { + return ( + + {LazyLogo && ( + + )} + + ); + } + + return ( + setIsFocused(false)} + /> + ); +}; + +// Inner component with all the editing logic - only rendered when focused +const EditableLogoCellInner = ({ row, logoId, onBlur }) => { + // Subscribe directly to the logos store so we get updates when logos load + const channelLogos = useLogosStore((s) => s.channelLogos); + const previousLogoId = useRef(logoId); const [searchValue, setSearchValue] = useState(''); const saveValue = useCallback( async (newLogoId) => { - // Don't save if not unlocked or value hasn't changed - if (!isUnlocked || String(newLogoId) === String(previousLogoId.current)) { + // Don't save if value hasn't changed + if (String(newLogoId) === String(previousLogoId.current)) { return; } @@ -628,20 +718,13 @@ export const EditableLogoCell = ({ row, getValue, channelLogos, LazyLogo }) => { console.error('Failed to update logo:', error); } }, - [row.original.id, isUnlocked] + [row.original.id] ); - const handleClick = () => { - if (isUnlocked) { - setSearchValue(''); - setIsFocused(true); - } - }; - const handleChange = (newLogoId) => { saveValue(newLogoId); setSearchValue(''); - setIsFocused(false); + onBlur(); }; // Build logo options with logo data @@ -706,36 +789,6 @@ export const EditableLogoCell = ({ row, getValue, channelLogos, LazyLogo }) => { ); }; - if (!isUnlocked || !isFocused) { - // When not editing, show the logo image - return ( - - {LazyLogo && ( - - )} - - ); - } - return ( {