From d7b98fef8d3da676c0dd0f0cd015afc784bc5dce Mon Sep 17 00:00:00 2001 From: None Date: Sat, 31 Jan 2026 23:11:20 -0600 Subject: [PATCH] Add default/advanced mode toggle and test coverage for EPG matching Enhanced EPG matching UX by adding radio button toggle to more clearly define optional advanced options. Also added test coverage for stability. Frontend Changes: - Added radio button UI to EPGMatchModal for default/advanced mode selection - Default mode: Uses built-in normalization - Advanced mode: Shows TagsInput fields for custom prefixes/suffixes/strings - Mode persists across sessions and settings are preserved when switching modes Test Coverage: - Created EPGMatchModal.test.jsx with test cases covering: - Component rendering and mode switching - Form submission and settings persistence - Error handling and UI text variations - Added test cases to SettingsUtils.test.js for EPG mode handling: - epg_match_mode routing to epg_settings group - Settings creation and preservation Since advanced settings are saved persistently but ignored when default settings are used, this adds an easy way in the future to add additional advanced settings to EPG matching like region influence, match aggression, and match preview, since users can easily switch between modes. --- apps/channels/tasks.py | 28 ++- core/models.py | 1 + .../src/components/modals/EPGMatchModal.jsx | 141 ++++++++----- .../modals/__tests__/EPGMatchModal.test.jsx | 190 ++++++++++++++++++ frontend/src/utils/pages/SettingsUtils.js | 8 +- .../pages/__tests__/SettingsUtils.test.js | 126 ++++++++++++ 6 files changed, 429 insertions(+), 65 deletions(-) create mode 100644 frontend/src/components/modals/__tests__/EPGMatchModal.test.jsx diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py index 25c0bfa0..8d49287b 100755 --- a/apps/channels/tasks.py +++ b/apps/channels/tasks.py @@ -139,7 +139,7 @@ COMMON_EXTRANEOUS_WORDS = [ def normalize_name(name: str) -> str: """ A more aggressive normalization that: - - Removes user-configured prefixes/suffixes/custom strings (if configured) + - Removes user-configured prefixes/suffixes/custom strings (only if mode is 'advanced') - Lowercases - Removes bracketed/parenthesized text - Removes punctuation @@ -157,17 +157,23 @@ def normalize_name(name: str) -> str: try: from core.models import CoreSettings settings = CoreSettings.get_epg_settings() - prefixes = settings.get("epg_match_ignore_prefixes", []) - suffixes = settings.get("epg_match_ignore_suffixes", []) - custom_strings = settings.get("epg_match_ignore_custom", []) - # Ensure we have lists - if not isinstance(prefixes, list): - prefixes = [] - if not isinstance(suffixes, list): - suffixes = [] - if not isinstance(custom_strings, list): - custom_strings = [] + # Check if user has enabled advanced mode + mode = settings.get("epg_match_mode", "default") + + # Only use custom settings if mode is 'advanced' + if mode == "advanced": + prefixes = settings.get("epg_match_ignore_prefixes", []) + suffixes = settings.get("epg_match_ignore_suffixes", []) + custom_strings = settings.get("epg_match_ignore_custom", []) + + # Ensure we have lists + if not isinstance(prefixes, list): + prefixes = [] + if not isinstance(suffixes, list): + suffixes = [] + if not isinstance(custom_strings, list): + custom_strings = [] except Exception as e: # Settings unavailable or error - continue with empty lists (graceful degradation) diff --git a/core/models.py b/core/models.py index 1037a6e3..c72192db 100644 --- a/core/models.py +++ b/core/models.py @@ -233,6 +233,7 @@ class CoreSettings(models.Model): def get_epg_settings(cls): """Get all EPG-related settings.""" return cls._get_group(EPG_SETTINGS_KEY, { + "epg_match_mode": "default", "epg_match_ignore_prefixes": [], "epg_match_ignore_suffixes": [], "epg_match_ignore_custom": [], diff --git a/frontend/src/components/modals/EPGMatchModal.jsx b/frontend/src/components/modals/EPGMatchModal.jsx index fc773d94..64873643 100644 --- a/frontend/src/components/modals/EPGMatchModal.jsx +++ b/frontend/src/components/modals/EPGMatchModal.jsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState, useEffect, useRef } from 'react'; import { Modal, Stack, @@ -7,6 +7,7 @@ import { Group, Button, Loader, + Radio, } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import useSettingsStore from '../../store/settings'; @@ -20,6 +21,7 @@ import { const getEpgSettingsFromStore = (settings) => { const epgSettings = settings?.['epg_settings']?.value; return { + epg_match_mode: epgSettings?.epg_match_mode || 'default', epg_match_ignore_prefixes: Array.isArray(epgSettings?.epg_match_ignore_prefixes) ? epgSettings.epg_match_ignore_prefixes : [], @@ -40,6 +42,7 @@ const EPGMatchModal = ({ const settings = useSettingsStore((s) => s.settings); const [loading, setLoading] = useState(false); + const [settingsMode, setSettingsMode] = useState('default'); // Compute form values directly from settings - memoized for performance const storedValues = useMemo( @@ -50,18 +53,28 @@ const EPGMatchModal = ({ // Local form state const [formValues, setFormValues] = useState(storedValues); - // Reset to stored values when modal opens + // Track previous opened state to detect transitions + const prevOpened = useRef(false); + + // Reset to stored values and mode only when modal opens (not on storedValues changes) useEffect(() => { - if (opened) { + // Only reset when transitioning from closed to open + if (opened && !prevOpened.current) { setFormValues(storedValues); + setSettingsMode(storedValues.epg_match_mode); } + prevOpened.current = opened; }, [opened, storedValues]); const handleConfirm = async () => { setLoading(true); try { - // Save settings first - const changedSettings = getChangedSettings(formValues, settings); + // Save mode and settings (backend will ignore custom settings if mode is 'default') + const settingsToSave = { + ...formValues, + epg_match_mode: settingsMode, + }; + const changedSettings = getChangedSettings(settingsToSave, settings); if (Object.keys(changedSettings).length > 0) { await saveChangedSettings(settings, changedSettings); } @@ -108,59 +121,81 @@ const EPGMatchModal = ({ > - Configure how channel names are normalized during matching, then start - the auto-match process for {scopeText}. + Match channels to EPG data for {scopeText}. - - setFormValues((prev) => ({ - ...prev, - epg_match_ignore_prefixes: value, - })) - } - splitChars={[]} - clearable - /> + + + + + + - - setFormValues((prev) => ({ - ...prev, - epg_match_ignore_suffixes: value, - })) - } - splitChars={[]} - clearable - /> + {settingsMode === 'advanced' && ( + <> + + setFormValues((prev) => ({ + ...prev, + epg_match_ignore_prefixes: value, + })) + } + splitChars={[]} + clearable + /> - - setFormValues((prev) => ({ - ...prev, - epg_match_ignore_custom: value, - })) - } - splitChars={[]} - clearable - /> + + setFormValues((prev) => ({ + ...prev, + epg_match_ignore_suffixes: value, + })) + } + splitChars={[]} + clearable + /> - - Channel display names are never modified. These settings only affect - the matching algorithm. - + + setFormValues((prev) => ({ + ...prev, + epg_match_ignore_custom: value, + })) + } + splitChars={[]} + clearable + /> + + + Channel display names are never modified. These settings only affect + the matching algorithm. + + + )}