diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index 052b9733..05462d0f 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -55,6 +55,10 @@ class M3UAccountProfileSerializer(serializers.ModelSerializer): "account", ] read_only_fields = ["id", "account"] + extra_kwargs = { + 'search_pattern': {'required': False, 'allow_blank': True}, + 'replace_pattern': {'required': False, 'allow_blank': True}, + } def create(self, validated_data): m3u_account = self.context.get("m3u_account") @@ -64,9 +68,39 @@ class M3UAccountProfileSerializer(serializers.ModelSerializer): return super().create(validated_data) + def validate(self, data): + """Custom validation to handle default profiles""" + # For updates to existing instances + if self.instance and self.instance.is_default: + # For default profiles, search_pattern and replace_pattern are not required + # and we don't want to validate them since they shouldn't be changed + return data + + # For non-default profiles or new profiles, ensure required fields are present + if not data.get('search_pattern'): + raise serializers.ValidationError({ + 'search_pattern': ['This field is required for non-default profiles.'] + }) + if not data.get('replace_pattern'): + raise serializers.ValidationError({ + 'replace_pattern': ['This field is required for non-default profiles.'] + }) + + return data + def update(self, instance, validated_data): if instance.is_default: - raise serializers.ValidationError("Default profiles cannot be modified.") + # For default profiles, only allow updating name and custom_properties (for notes) + allowed_fields = {'name', 'custom_properties'} + + # Remove any fields that aren't allowed for default profiles + disallowed_fields = set(validated_data.keys()) - allowed_fields + if disallowed_fields: + raise serializers.ValidationError( + f"Default profiles can only modify name and notes. " + f"Cannot modify: {', '.join(disallowed_fields)}" + ) + return super().update(instance, validated_data) def destroy(self, request, *args, **kwargs): diff --git a/frontend/src/components/forms/AccountInfoModal.jsx b/frontend/src/components/forms/AccountInfoModal.jsx index fa44df90..b8522638 100644 --- a/frontend/src/components/forms/AccountInfoModal.jsx +++ b/frontend/src/components/forms/AccountInfoModal.jsx @@ -284,6 +284,30 @@ const AccountInfoModal = ({ isOpen, onClose, profile, onRefresh }) => { + {/* Profile Notes */} + {currentProfile?.custom_properties?.notes && ( + <> + + + + Profile Notes + + + + {currentProfile.custom_properties.notes} + + + + + )} + {/* Detailed Information Table */} diff --git a/frontend/src/components/forms/M3UProfile.jsx b/frontend/src/components/forms/M3UProfile.jsx index ac6adca2..353e48d1 100644 --- a/frontend/src/components/forms/M3UProfile.jsx +++ b/frontend/src/components/forms/M3UProfile.jsx @@ -12,6 +12,8 @@ import { Paper, Badge, Grid, + Textarea, + NumberInput, } from '@mantine/core'; import { useWebSocket } from '../../WebSocket'; import usePlaylistsStore from '../../store/playlists'; @@ -27,6 +29,7 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => { const [replacePattern, setReplacePattern] = useState(''); const [debouncedPatterns, setDebouncedPatterns] = useState({}); const [sampleInput, setSampleInput] = useState(''); + const isDefaultProfile = profile?.is_default; useEffect(() => { async function fetchStreamUrl() { @@ -91,21 +94,58 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => { max_streams: 0, search_pattern: '', replace_pattern: '', + notes: '', }, validationSchema: Yup.object({ name: Yup.string().required('Name is required'), - search_pattern: Yup.string().required('Search pattern is required'), - replace_pattern: Yup.string().required('Replace pattern is required'), + search_pattern: Yup.string().when([], { + is: () => !isDefaultProfile, + then: (schema) => schema.required('Search pattern is required'), + otherwise: (schema) => schema.notRequired(), + }), + replace_pattern: Yup.string().when([], { + is: () => !isDefaultProfile, + then: (schema) => schema.required('Replace pattern is required'), + otherwise: (schema) => schema.notRequired(), + }), + notes: Yup.string(), // Optional field }), onSubmit: async (values, { setSubmitting, resetForm }) => { console.log('submiting'); + + // For default profiles, only send name and custom_properties (notes) + let submitValues; + if (isDefaultProfile) { + submitValues = { + name: values.name, + custom_properties: { + // Preserve existing custom_properties and add/update notes + ...(profile?.custom_properties || {}), + notes: values.notes || '', + }, + }; + } else { + // For regular profiles, send all fields + submitValues = { + name: values.name, + max_streams: values.max_streams, + search_pattern: values.search_pattern, + replace_pattern: values.replace_pattern, + custom_properties: { + // Preserve existing custom_properties and add/update notes + ...(profile?.custom_properties || {}), + notes: values.notes || '', + }, + }; + } + if (profile?.id) { await API.updateM3UProfile(m3u.id, { id: profile.id, - ...values, + ...submitValues, }); } else { - await API.addM3UProfile(m3u.id, values); + await API.addM3UProfile(m3u.id, submitValues); } resetForm(); @@ -123,11 +163,12 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => { max_streams: profile.max_streams, search_pattern: profile.search_pattern, replace_pattern: profile.replace_pattern, + notes: profile.custom_properties?.notes || '', }); } else { formik.resetForm(); } - }, [profile]); + }, [profile]); // eslint-disable-line react-hooks/exhaustive-deps const handleSampleInputChange = (e) => { setSampleInput(e.target.value); @@ -142,7 +183,7 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => { regex, (match) => `${match}` ); - } catch (e) { + } catch { return sampleInput; } }; @@ -152,13 +193,22 @@ const RegexFormAndView = ({ profile = null, m3u, isOpen, onClose }) => { try { const regex = new RegExp(searchPattern, 'g'); return sampleInput.replace(regex, replacePattern); - } catch (e) { + } catch { return sampleInput; } }; return ( - +
{ onChange={formik.handleChange} error={formik.errors.name ? formik.touched.name : ''} /> - + formik.setFieldValue('max_streams', value || 0) + } + error={formik.errors.max_streams ? formik.touched.max_streams : ''} + min={0} + placeholder="0 = unlimited" + /> + )} + + {/* Only show search/replace fields for non-default profiles */} + {!isDefaultProfile && ( + <> + + + + )} + +