mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Perf: lazy load editable cells on focus, not on unlock. Also don't wait for all channels to load before logging in.
This commit is contained in:
parent
716dc25c52
commit
e2a915f10b
3 changed files with 320 additions and 271 deletions
|
|
@ -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) => (
|
||||
<Box
|
||||
onClick={() => {
|
||||
// Ensure logos are loaded when user tries to edit
|
||||
ensureLogosLoaded();
|
||||
}}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<EditableLogoCell
|
||||
{...props}
|
||||
channelLogos={channelLogos}
|
||||
LazyLogo={LazyLogo}
|
||||
/>
|
||||
</Box>
|
||||
<EditableLogoCell
|
||||
{...props}
|
||||
LazyLogo={LazyLogo}
|
||||
ensureLogosLoaded={ensureLogosLoaded}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
) : (
|
||||
<Box
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '0 4px',
|
||||
}}
|
||||
>
|
||||
{getValue() ?? ''}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
// 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 (
|
||||
<Box
|
||||
onClick={() => isUnlocked && setIsFocused(true)}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: isUnlocked ? 'text' : 'default',
|
||||
padding: '0 4px',
|
||||
}}
|
||||
>
|
||||
{getValue() ?? ''}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Only mount heavy component when actually editing
|
||||
return (
|
||||
<EditableTextCellInner
|
||||
row={row}
|
||||
column={column}
|
||||
getValue={getValue}
|
||||
onBlur={() => 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 (
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: isUnlocked ? 'text' : 'default',
|
||||
padding: '0 4px',
|
||||
...(isUnlocked && {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
value={value}
|
||||
|
|
@ -154,28 +179,62 @@ export const EditableTextCell = ({ row, column, getValue }) => {
|
|||
// 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 (
|
||||
<Box
|
||||
onClick={() => isUnlocked && setIsFocused(true)}
|
||||
style={{
|
||||
textAlign: 'right',
|
||||
width: '100%',
|
||||
cursor: isUnlocked ? 'text' : 'default',
|
||||
padding: '0 4px',
|
||||
}}
|
||||
>
|
||||
{formattedValue}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableNumberCellInner
|
||||
row={row}
|
||||
column={column}
|
||||
getValue={getValue}
|
||||
onBlur={() => 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 (
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
textAlign: 'right',
|
||||
width: '100%',
|
||||
cursor: isUnlocked ? 'text' : 'default',
|
||||
padding: '0 4px',
|
||||
...(isUnlocked && {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{formattedValue}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
value={value}
|
||||
|
|
@ -291,21 +312,56 @@ export const EditableNumberCell = ({ row, column, getValue }) => {
|
|||
};
|
||||
|
||||
// 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 (
|
||||
<Box
|
||||
onClick={() => isUnlocked && setIsFocused(true)}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '0 4px',
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
}}
|
||||
>
|
||||
{groupName}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableGroupCellInner
|
||||
row={row}
|
||||
channelGroups={channelGroups}
|
||||
groupName={groupName}
|
||||
groupId={groupId}
|
||||
onBlur={() => 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 (
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
padding: '0 4px',
|
||||
...(isUnlocked && {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{groupName}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={null}
|
||||
onChange={handleChange}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onBlur={onBlur}
|
||||
data={groupOptions}
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
|
|
@ -401,12 +429,10 @@ export const EditableEPGCell = ({
|
|||
tvgsLoaded,
|
||||
}) => {
|
||||
const isUnlocked = useChannelsTableStore((s) => s.isUnlocked);
|
||||
const epgDataId = getValue();
|
||||
const previousEpgDataId = useRef(epgDataId);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const epgDataId = getValue();
|
||||
|
||||
// Format display text
|
||||
// Format display text - needed for both locked and unlocked states
|
||||
const epgObj = epgDataId ? tvgsById[epgDataId] : null;
|
||||
const tvgId = epgObj?.tvg_id;
|
||||
const epgName =
|
||||
|
|
@ -423,13 +449,90 @@ export const EditableEPGCell = ({
|
|||
// Show skeleton while EPG data is loading (only if channel has an EPG assignment)
|
||||
const isEpgDataPending = epgDataId && !epgObj && !tvgsLoaded;
|
||||
|
||||
// Build tooltip content
|
||||
const tooltip = epgObj
|
||||
? `${epgName ? `EPG Name: ${epgName}\n` : ''}${epgObj.name ? `TVG Name: ${epgObj.name}\n` : ''}${tvgId ? `TVG-ID: ${tvgId}` : ''}`.trim()
|
||||
: '';
|
||||
|
||||
// Show simple display when locked OR when unlocked but not focused
|
||||
if (!isUnlocked || !isFocused) {
|
||||
// If loading EPG data, show skeleton
|
||||
if (isEpgDataPending) {
|
||||
return (
|
||||
<Box
|
||||
onClick={() => isUnlocked && setIsFocused(true)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 4px',
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
height={18}
|
||||
width="70%"
|
||||
visible={true}
|
||||
animate={true}
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
label={<span style={{ whiteSpace: 'pre-line' }}>{tooltip}</span>}
|
||||
withArrow
|
||||
position="top"
|
||||
disabled={!epgObj}
|
||||
openDelay={500}
|
||||
>
|
||||
<Box
|
||||
onClick={() => isUnlocked && setIsFocused(true)}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
padding: '0 4px',
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableEPGCellInner
|
||||
row={row}
|
||||
tvgsById={tvgsById}
|
||||
epgs={epgs}
|
||||
epgDataId={epgDataId}
|
||||
epgObj={epgObj}
|
||||
displayText={displayText}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Inner component with all the editing logic - only rendered when focused
|
||||
const EditableEPGCellInner = ({
|
||||
row,
|
||||
tvgsById,
|
||||
epgs,
|
||||
epgDataId,
|
||||
displayText,
|
||||
onBlur,
|
||||
}) => {
|
||||
const previousEpgDataId = useRef(epgDataId);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const saveValue = useCallback(
|
||||
async (newEpgDataId) => {
|
||||
// Don't save if not unlocked or value hasn't changed
|
||||
if (
|
||||
!isUnlocked ||
|
||||
String(newEpgDataId) === String(previousEpgDataId.current)
|
||||
) {
|
||||
// Don't save if value hasn't changed
|
||||
if (String(newEpgDataId) === String(previousEpgDataId.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -449,20 +552,13 @@ export const EditableEPGCell = ({
|
|||
console.error('Failed to update EPG:', error);
|
||||
}
|
||||
},
|
||||
[row.original.id, isUnlocked]
|
||||
[row.original.id]
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isUnlocked) {
|
||||
setSearchValue(''); // Start with empty search
|
||||
setIsFocused(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (newEpgDataId) => {
|
||||
saveValue(newEpgDataId);
|
||||
setSearchValue('');
|
||||
setIsFocused(false);
|
||||
onBlur();
|
||||
};
|
||||
|
||||
// Build EPG options
|
||||
|
|
@ -514,69 +610,11 @@ export const EditableEPGCell = ({
|
|||
return options;
|
||||
}, [tvgsById, epgs]);
|
||||
|
||||
// Build tooltip content
|
||||
const tooltip = epgObj
|
||||
? `${epgName ? `EPG Name: ${epgName}\n` : ''}${epgObj.name ? `TVG Name: ${epgObj.name}\n` : ''}${tvgId ? `TVG-ID: ${tvgId}` : ''}`.trim()
|
||||
: '';
|
||||
|
||||
if (!isUnlocked || !isFocused) {
|
||||
// If loading EPG data, show skeleton
|
||||
if (isEpgDataPending) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 4px',
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
height={18}
|
||||
width="70%"
|
||||
visible={true}
|
||||
animate={true}
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
// Otherwise, show the normal EPG assignment cell
|
||||
return (
|
||||
<Tooltip
|
||||
label={<span style={{ whiteSpace: 'pre-line' }}>{tooltip}</span>}
|
||||
withArrow
|
||||
position="top"
|
||||
disabled={!epgObj}
|
||||
openDelay={500}
|
||||
>
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
padding: '0 4px',
|
||||
...(isUnlocked && {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{displayText}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={null}
|
||||
onChange={handleChange}
|
||||
onBlur={() => 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 (
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
}}
|
||||
>
|
||||
{LazyLogo && (
|
||||
<LazyLogo
|
||||
logoId={logoId}
|
||||
alt="logo"
|
||||
style={{ maxHeight: 18, maxWidth: 55 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableLogoCellInner
|
||||
row={row}
|
||||
logoId={logoId}
|
||||
onBlur={() => 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 (
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
cursor: isUnlocked ? 'pointer' : 'default',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...(isUnlocked && {
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{LazyLogo && (
|
||||
<LazyLogo
|
||||
logoId={logoId}
|
||||
alt="logo"
|
||||
style={{ maxHeight: 18, maxWidth: 55 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
|
|
@ -748,7 +801,7 @@ export const EditableLogoCell = ({ row, getValue, channelLogos, LazyLogo }) => {
|
|||
<Select
|
||||
value={null}
|
||||
onChange={handleChange}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onBlur={onBlur}
|
||||
data={logoOptions}
|
||||
size="xs"
|
||||
variant="unstyled"
|
||||
|
|
|
|||
|
|
@ -56,9 +56,10 @@ const useAuthStore = create((set, get) => ({
|
|||
// Ensure settings are loaded first
|
||||
await useSettingsStore.getState().fetchSettings();
|
||||
|
||||
// Only after settings are loaded, fetch the essential data
|
||||
// Fetch essential data needed for initial render
|
||||
// Note: fetchChannels() is intentionally NOT awaited here - it's slow (~3s)
|
||||
// and only needed for delete modal details. It loads in background after UI renders.
|
||||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
useChannelsStore.getState().fetchChannelGroups(),
|
||||
useChannelsStore.getState().fetchChannelProfiles(),
|
||||
usePlaylistsStore.getState().fetchPlaylists(),
|
||||
|
|
@ -72,7 +73,7 @@ const useAuthStore = create((set, get) => ({
|
|||
await Promise.all([useUsersStore.getState().fetchUsers()]);
|
||||
}
|
||||
|
||||
// Only set isAuthenticated and isInitialized AFTER all data is loaded
|
||||
// Only set isAuthenticated and isInitialized AFTER essential data is loaded
|
||||
// This prevents routes from rendering before data is ready
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
|
|
@ -80,6 +81,9 @@ const useAuthStore = create((set, get) => ({
|
|||
isInitializing: false,
|
||||
});
|
||||
|
||||
// Load channels data in background (not blocking) - needed for delete modal details
|
||||
useChannelsStore.getState().fetchChannels();
|
||||
|
||||
// Note: Logos are loaded after the Channels page tables finish loading
|
||||
// This is handled by the tables themselves signaling completion
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue