Minor formatting changes

This commit is contained in:
Nick Sandstrom 2026-05-11 20:44:12 -07:00
parent 64f19237f9
commit 4dace2d333
10 changed files with 158 additions and 93 deletions

View file

@ -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"
/>

View file

@ -285,8 +285,8 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => {
>
<Text size="sm">
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.
</Text>
</Alert>
<TextInput

View file

@ -1,12 +1,5 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import {
Box,
Button,
Text,
Group,
ActionIcon,
Stack,
} from '@mantine/core';
import { Box, Button, Text, Group, ActionIcon, Stack } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { GripVertical, Eye, EyeOff } from 'lucide-react';
import {
@ -36,7 +29,14 @@ import {
import { USER_LEVELS } from '../../../constants';
const DraggableNavItem = ({ item, isHidden, canHide, onToggleVisibility }) => {
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 }) => {
>
<GripVertical size={16} color="#888" />
</ActionIcon>
{IconComponent && <IconComponent size={18} color={isHidden ? '#666' : '#ccc'} />}
{IconComponent && (
<IconComponent size={18} color={isHidden ? '#666' : '#ccc'} />
)}
<Text size="sm" c={isHidden ? 'dimmed' : 'gray.3'}>
{item.label}
</Text>
@ -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

View file

@ -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 }) => {
))}
<Flex mih={50} gap="xs" justify="space-between" align="flex-end">
<Button variant="subtle" color="gray" onClick={resetNetworkAccessToDefaults}>
<Button
variant="subtle"
color="gray"
onClick={resetNetworkAccessToDefaults}
>
Reset to Defaults
</Button>
<Button type="submit" disabled={networkAccessForm.submitting} variant="default">
<Button
type="submit"
disabled={networkAccessForm.submitting}
variant="default"
>
Save
</Button>
</Flex>

View file

@ -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 }) => (
<div>
<input aria-label={label} placeholder={placeholder} data-error={error} readOnly />
<input
aria-label={label}
placeholder={placeholder}
data-error={error}
readOnly
/>
{error && <span>{error}</span>}
</div>
),

View file

@ -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';

View file

@ -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' }]);

View file

@ -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 () => {

View file

@ -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;
}, {});

View file

@ -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'
);