diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 90b95a42..ee8c4e91 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -931,7 +931,10 @@ const ChannelForm = ({ channel: channelProp = null, isOpen, onClose }) => { label="Hidden" checked={watch('hidden_from_output')} onChange={(event) => - setValue('hidden_from_output', event.currentTarget.checked) + setValue( + 'hidden_from_output', + event.currentTarget.checked + ) } size="md" /> diff --git a/frontend/src/components/forms/M3UProfile.jsx b/frontend/src/components/forms/M3UProfile.jsx index 72787d37..4fdd7e0f 100644 --- a/frontend/src/components/forms/M3UProfile.jsx +++ b/frontend/src/components/forms/M3UProfile.jsx @@ -285,8 +285,8 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => { > These patterns are applied to every stream in this playlist. If - the search pattern doesn't match a stream URL, the original - URL is used as-is. + the search pattern doesn't match a stream URL, the original URL + is used as-is. { - const { transform, transition, setNodeRef, isDragging, attributes, listeners } = useSortable({ + const { + transform, + transition, + setNodeRef, + isDragging, + attributes, + listeners, + } = useSortable({ id: item.id, }); @@ -73,7 +73,9 @@ const DraggableNavItem = ({ item, isHidden, canHide, onToggleVisibility }) => { > - {IconComponent && } + {IconComponent && ( + + )} {item.label} @@ -140,45 +142,48 @@ const NavOrderForm = ({ active }) => { }, []); // Debounced save function - const debouncedSave = useCallback(async (newOrder) => { - // Clear any pending save - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - - // Store the pending order - pendingOrderRef.current = newOrder; - - // Schedule save after 800ms of inactivity - saveTimeoutRef.current = setTimeout(async () => { - const orderToSave = pendingOrderRef.current; - if (!orderToSave) return; - - setIsSaving(true); - try { - await setNavOrder(orderToSave); - notifications.show({ - title: 'Navigation', - message: 'Order saved successfully', - color: 'green', - autoClose: 2000, - }); - } catch { - // Revert on failure - const savedOrder = getNavOrder(); - const orderedItems = getOrderedNavItems(savedOrder, isAdmin); - setItems(orderedItems); - notifications.show({ - title: 'Error', - message: 'Failed to save navigation order', - color: 'red', - }); - } finally { - setIsSaving(false); - pendingOrderRef.current = null; + const debouncedSave = useCallback( + async (newOrder) => { + // Clear any pending save + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); } - }, 800); - }, [setNavOrder, getNavOrder, isAdmin]); + + // Store the pending order + pendingOrderRef.current = newOrder; + + // Schedule save after 800ms of inactivity + saveTimeoutRef.current = setTimeout(async () => { + const orderToSave = pendingOrderRef.current; + if (!orderToSave) return; + + setIsSaving(true); + try { + await setNavOrder(orderToSave); + notifications.show({ + title: 'Navigation', + message: 'Order saved successfully', + color: 'green', + autoClose: 2000, + }); + } catch { + // Revert on failure + const savedOrder = getNavOrder(); + const orderedItems = getOrderedNavItems(savedOrder, isAdmin); + setItems(orderedItems); + notifications.show({ + title: 'Error', + message: 'Failed to save navigation order', + color: 'red', + }); + } finally { + setIsSaving(false); + pendingOrderRef.current = null; + } + }, 800); + }, + [setNavOrder, getNavOrder, isAdmin] + ); const handleDragEnd = ({ active, over }) => { if (!over || active.id === over.id) return; @@ -196,23 +201,26 @@ const NavOrderForm = ({ active }) => { }; // Wrapped visibility toggle with error handling - const handleToggleVisibility = useCallback(async (itemId) => { - try { - await toggleNavVisibility(itemId); - notifications.show({ - title: 'Navigation', - message: 'Visibility updated', - color: 'green', - autoClose: 2000, - }); - } catch { - notifications.show({ - title: 'Error', - message: 'Failed to update visibility', - color: 'red', - }); - } - }, [toggleNavVisibility]); + const handleToggleVisibility = useCallback( + async (itemId) => { + try { + await toggleNavVisibility(itemId); + notifications.show({ + title: 'Navigation', + message: 'Visibility updated', + color: 'green', + autoClose: 2000, + }); + } catch { + notifications.show({ + title: 'Error', + message: 'Failed to update visibility', + color: 'red', + }); + } + }, + [toggleNavVisibility] + ); const handleReset = async () => { // Cancel any pending debounced save diff --git a/frontend/src/components/forms/settings/NetworkAccessForm.jsx b/frontend/src/components/forms/settings/NetworkAccessForm.jsx index 2c5ba03a..97c5243d 100644 --- a/frontend/src/components/forms/settings/NetworkAccessForm.jsx +++ b/frontend/src/components/forms/settings/NetworkAccessForm.jsx @@ -14,7 +14,13 @@ import { getNetworkAccessDefaults, } from '../../../utils/forms/settings/NetworkAccessFormUtils.js'; -const toTags = (str) => (str ? str.split(',').map((s) => s.trim()).filter(Boolean) : []); +const toTags = (str) => + str + ? str + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + : []; const toStr = (tags) => (tags || []).join(','); const NetworkAccessForm = React.memo(({ active }) => { @@ -116,9 +122,14 @@ const NetworkAccessForm = React.memo(({ active }) => { const saveNetworkAccess = async () => { setSaved(false); setSaving(true); - const values = pendingSaveValuesRef.current || Object.fromEntries( - Object.entries(networkAccessForm.getValues()).map(([k, v]) => [k, toStr(v)]) - ); + const values = + pendingSaveValuesRef.current || + Object.fromEntries( + Object.entries(networkAccessForm.getValues()).map(([k, v]) => [ + k, + toStr(v), + ]) + ); try { await updateSetting({ ...settings['network_access'], @@ -166,10 +177,18 @@ const NetworkAccessForm = React.memo(({ active }) => { ))} - - diff --git a/frontend/src/components/forms/settings/__tests__/NetworkAccessForm.test.jsx b/frontend/src/components/forms/settings/__tests__/NetworkAccessForm.test.jsx index 24cea70c..a1b7fdd1 100644 --- a/frontend/src/components/forms/settings/__tests__/NetworkAccessForm.test.jsx +++ b/frontend/src/components/forms/settings/__tests__/NetworkAccessForm.test.jsx @@ -5,7 +5,10 @@ import NetworkAccessForm from '../NetworkAccessForm'; // ── Constants mock ───────────────────────────────────────────────────────────── vi.mock('../../../../constants.js', () => ({ NETWORK_ACCESS_OPTIONS: { - M3U_EPG: { label: 'M3U / EPG Endpoints', description: 'Limit M3U/EPG access' }, + M3U_EPG: { + label: 'M3U / EPG Endpoints', + description: 'Limit M3U/EPG access', + }, STREAMS: { label: 'Stream Endpoints', description: 'Limit stream access' }, XC_API: { label: 'XC API', description: 'Limit XC API access' }, UI: { label: 'UI', description: 'Limit UI access' }, @@ -85,7 +88,12 @@ vi.mock('@mantine/core', () => ({ ), TagsInput: ({ label, placeholder, error, ...rest }) => (
- + {error && {error}}
), diff --git a/frontend/src/utils/forms/ChannelBatchUtils.js b/frontend/src/utils/forms/ChannelBatchUtils.js index cebe5990..d603e5f4 100644 --- a/frontend/src/utils/forms/ChannelBatchUtils.js +++ b/frontend/src/utils/forms/ChannelBatchUtils.js @@ -191,7 +191,10 @@ export const buildSubmitValues = ( values.is_adult = values.is_adult === 'true'; } - if (values.hidden_from_output === '-1' || values.hidden_from_output === undefined) { + if ( + values.hidden_from_output === '-1' || + values.hidden_from_output === undefined + ) { delete values.hidden_from_output; } else { values.hidden_from_output = values.hidden_from_output === 'true'; diff --git a/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js b/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js index 00e4f907..e48b4213 100644 --- a/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js +++ b/frontend/src/utils/forms/__tests__/ChannelBatchUtils.bulkOverrideRouting.test.js @@ -30,7 +30,7 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => { await updateChannelsWithOverrideRouting( [1, 2], { name: 'BulkRename' }, - channelsById, + channelsById ); expect(API.bulkUpdateChannels).toHaveBeenCalledTimes(1); @@ -48,7 +48,7 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => { await updateChannelsWithOverrideRouting( [1], { name: 'ManualRename' }, - channelsById, + channelsById ); const body = API.bulkUpdateChannels.mock.calls[0][0]; expect(body).toEqual([{ id: 1, name: 'ManualRename' }]); @@ -63,7 +63,7 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => { await updateChannelsWithOverrideRouting( [1, 2, 3], { name: 'Mixed', tvg_id: 'mixed.tvg' }, - channelsById, + channelsById ); const body = API.bulkUpdateChannels.mock.calls[0][0]; expect(body).toEqual([ @@ -82,7 +82,7 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => { await updateChannelsWithOverrideRouting( [1], { hidden_from_output: true, name: 'Renamed' }, - channelsById, + channelsById ); const body = API.bulkUpdateChannels.mock.calls[0][0]; expect(body).toEqual([ @@ -97,7 +97,7 @@ describe('updateChannelsWithOverrideRouting (manual finding #4)', () => { await updateChannelsWithOverrideRouting( [99], { name: 'Defensive' }, - {}, // empty lookup + {} // empty lookup ); const body = API.bulkUpdateChannels.mock.calls[0][0]; expect(body).toEqual([{ id: 99, name: 'Defensive' }]); diff --git a/frontend/src/utils/forms/__tests__/M3uProfileUtils.test.js b/frontend/src/utils/forms/__tests__/M3uProfileUtils.test.js index 92fb6416..9324d58c 100644 --- a/frontend/src/utils/forms/__tests__/M3uProfileUtils.test.js +++ b/frontend/src/utils/forms/__tests__/M3uProfileUtils.test.js @@ -71,7 +71,10 @@ describe('M3uProfileUtils', () => { await updateM3UProfile(m3u.id, makeSubmitValues()); - expect(API.updateM3UProfile).toHaveBeenCalledWith(m3u.id, expect.anything()); + expect(API.updateM3UProfile).toHaveBeenCalledWith( + m3u.id, + expect.anything() + ); }); it('passes submitValues through unmodified', async () => { @@ -153,9 +156,9 @@ describe('M3uProfileUtils', () => { vi.mocked(API.addM3UProfile).mockRejectedValue(new Error('Add failed')); const m3u = makeM3U(); - await expect( - addM3UProfile(m3u.id, makeSubmitValues()) - ).rejects.toThrow('Add failed'); + await expect(addM3UProfile(m3u.id, makeSubmitValues())).rejects.toThrow( + 'Add failed' + ); }); it('resolves without returning a value', async () => { @@ -195,7 +198,10 @@ describe('M3uProfileUtils', () => { await deleteM3UProfile(playlist.id, 'profile-1'); - expect(API.deleteM3UProfile).toHaveBeenCalledWith(playlist.id, expect.anything()); + expect(API.deleteM3UProfile).toHaveBeenCalledWith( + playlist.id, + expect.anything() + ); }); it('passes the profile id through unmodified', async () => { diff --git a/frontend/src/utils/forms/settings/NetworkAccessFormUtils.js b/frontend/src/utils/forms/settings/NetworkAccessFormUtils.js index bf9b6e3f..c5e57400 100644 --- a/frontend/src/utils/forms/settings/NetworkAccessFormUtils.js +++ b/frontend/src/utils/forms/settings/NetworkAccessFormUtils.js @@ -2,7 +2,15 @@ import { NETWORK_ACCESS_OPTIONS } from '../../../constants.js'; import { IPV4_CIDR_REGEX, IPV6_CIDR_REGEX } from '../../networkUtils.js'; // Default CIDR ranges for M3U/EPG endpoints (local networks only) -const M3U_EPG_DEFAULTS = ['127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '::1/128', 'fc00::/7', 'fe80::/10']; +const M3U_EPG_DEFAULTS = [ + '127.0.0.0/8', + '10.0.0.0/8', + '172.16.0.0/12', + '192.168.0.0/16', + '::1/128', + 'fc00::/7', + 'fe80::/10', +]; const OPEN_DEFAULTS = ['0.0.0.0/0', '::/0']; const isValidEntry = (entry) => @@ -22,7 +30,9 @@ export const getNetworkAccessFormValidation = () => Object.keys(NETWORK_ACCESS_OPTIONS).reduce((acc, key) => { acc[key] = (tags) => { if (!tags || tags.length === 0) return null; - return tags.some((t) => !isValidEntry(t)) ? 'Invalid IP address or CIDR range' : null; + return tags.some((t) => !isValidEntry(t)) + ? 'Invalid IP address or CIDR range' + : null; }; return acc; }, {}); diff --git a/frontend/src/utils/forms/settings/__tests__/NetworkAccessFormUtils.test.js b/frontend/src/utils/forms/settings/__tests__/NetworkAccessFormUtils.test.js index 2aa3ecfc..3b5a0866 100644 --- a/frontend/src/utils/forms/settings/__tests__/NetworkAccessFormUtils.test.js +++ b/frontend/src/utils/forms/settings/__tests__/NetworkAccessFormUtils.test.js @@ -109,9 +109,13 @@ describe('NetworkAccessFormUtils', () => { NetworkAccessFormUtils.getNetworkAccessFormValidation(); const validator = validation['network-access-admin']; - expect(validator(['192.168.1.256.1/24'])).toBe('Invalid IP address or CIDR range'); + expect(validator(['192.168.1.256.1/24'])).toBe( + 'Invalid IP address or CIDR range' + ); expect(validator(['invalid'])).toBe('Invalid IP address or CIDR range'); - expect(validator(['192.168.1.0/256'])).toBe('Invalid IP address or CIDR range'); + expect(validator(['192.168.1.0/256'])).toBe( + 'Invalid IP address or CIDR range' + ); }); it('should return error when any entry in the list is invalid', () => { @@ -119,8 +123,12 @@ describe('NetworkAccessFormUtils', () => { NetworkAccessFormUtils.getNetworkAccessFormValidation(); const validator = validation['network-access-admin']; - expect(validator(['192.168.1.0/24', 'invalid'])).toBe('Invalid IP address or CIDR range'); - expect(validator(['invalid', '192.168.1.0/24'])).toBe('Invalid IP address or CIDR range'); + expect(validator(['192.168.1.0/24', 'invalid'])).toBe( + 'Invalid IP address or CIDR range' + ); + expect(validator(['invalid', '192.168.1.0/24'])).toBe( + 'Invalid IP address or CIDR range' + ); expect(validator(['192.168.1.0/24', '10.0.0.0/8', 'invalid'])).toBe( 'Invalid IP address or CIDR range' );