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