fix: ensure data_to_sync has a safe structure and improve sync method handling

This commit is contained in:
Sam Chau
2025-01-08 16:17:40 +10:30
parent f3a483ecf2
commit ee8cf74384
3 changed files with 152 additions and 86 deletions

View File

@@ -39,6 +39,33 @@ const ArrModal = ({isOpen, onClose, onSubmit, editingArr}) => {
{value: 'schedule', label: 'Scheduled'}
];
// Ensure data_to_sync always has the required structure
const safeSelectedData = {
profiles: formData.data_to_sync?.profiles || [],
customFormats: formData.data_to_sync?.customFormats || []
};
// Handle sync method change
const handleSyncMethodChange = e => {
const newMethod = e.target.value;
handleInputChange({
target: {
id: 'sync_method',
value: newMethod
}
});
// Reset data_to_sync when switching to manual
if (newMethod === 'manual') {
handleInputChange({
target: {
id: 'data_to_sync',
value: {profiles: [], customFormats: []}
}
});
}
};
const inputClasses = errorKey =>
`w-full px-3 py-2 text-sm rounded-lg border ${
errors[errorKey]
@@ -261,7 +288,7 @@ const ArrModal = ({isOpen, onClose, onSubmit, editingArr}) => {
<select
id='sync_method'
value={formData.sync_method}
onChange={handleInputChange}
onChange={handleSyncMethodChange}
className={inputClasses('sync_method')}
required>
{syncMethods.map(m => (
@@ -305,35 +332,35 @@ const ArrModal = ({isOpen, onClose, onSubmit, editingArr}) => {
type='button'
onClick={() => setIsDataDrawerOpen(true)}
className='w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200
bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700
rounded-lg transition-colors
border border-gray-200 dark:border-gray-700'>
bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700
rounded-lg transition-colors
border border-gray-200 dark:border-gray-700'>
<div className='flex flex-col space-y-2'>
<div className='flex items-center justify-between'>
<span>Select Data to Sync</span>
</div>
{(formData.data_to_sync.profiles.length > 0 ||
formData.data_to_sync.customFormats.length >
{(safeSelectedData.profiles.length > 0 ||
safeSelectedData.customFormats.length >
0) && (
<div className='flex flex-wrap gap-2'>
{formData.data_to_sync.profiles.map(
{safeSelectedData.profiles.map(
profile => (
<span
key={profile}
className='inline-flex items-center bg-blue-100 text-blue-800
dark:bg-blue-900 dark:text-blue-300
text-xs rounded px-2 py-1'>
dark:bg-blue-900 dark:text-blue-300
text-xs rounded px-2 py-1'>
{profile}
</span>
)
)}
{formData.data_to_sync.customFormats.map(
{safeSelectedData.customFormats.map(
format => (
<span
key={format}
className='inline-flex items-center bg-green-100 text-green-800
dark:bg-green-900 dark:text-green-300
text-xs rounded px-2 py-1'>
dark:bg-green-900 dark:text-green-300
text-xs rounded px-2 py-1'>
{format}
</span>
)
@@ -388,7 +415,7 @@ const ArrModal = ({isOpen, onClose, onSubmit, editingArr}) => {
onClose={() => setIsDataDrawerOpen(false)}
isLoading={isLoading}
availableData={availableData}
selectedData={formData.data_to_sync}
selectedData={safeSelectedData}
onDataToggle={handleDataToggle}
error={errors.data_to_sync}
/>

View File

@@ -6,11 +6,15 @@ const DataSelectorModal = ({
isOpen,
onClose,
isLoading,
availableData,
selectedData,
availableData = {profiles: [], customFormats: []},
selectedData = {profiles: [], customFormats: []},
onDataToggle,
error
}) => {
// Ensure we have safe defaults for selectedData
const profiles = selectedData?.profiles || [];
const customFormats = selectedData?.customFormats || [];
return (
<Modal
isOpen={isOpen}
@@ -32,38 +36,40 @@ const DataSelectorModal = ({
Quality Profiles
</h4>
<span className='text-xs text-gray-500 dark:text-gray-400'>
{selectedData.profiles.length} selected
{profiles.length} selected
</span>
</div>
<div className='grid grid-cols-2 gap-2'>
{availableData.profiles.map(profile => (
<label
key={profile.file_name}
className='flex items-center p-2 bg-white dark:bg-gray-800
{(availableData?.profiles || []).map(
profile => (
<label
key={profile.file_name}
className='flex items-center p-2 bg-white dark:bg-gray-800
hover:bg-gray-50 dark:hover:bg-gray-700
rounded-lg cursor-pointer group transition-colors
border border-gray-200 dark:border-gray-700'>
<input
type='checkbox'
checked={selectedData.profiles.includes(
profile.content.name
)}
onChange={() =>
onDataToggle(
'profiles',
<input
type='checkbox'
checked={profiles.includes(
profile.content.name
)
}
className='rounded border-gray-300 text-blue-600
)}
onChange={() =>
onDataToggle(
'profiles',
profile.content.name
)
}
className='rounded border-gray-300 text-blue-600
focus:ring-blue-500 focus:ring-offset-0'
/>
<span
className='ml-3 text-sm text-gray-700 dark:text-gray-300
/>
<span
className='ml-3 text-sm text-gray-700 dark:text-gray-300
group-hover:text-gray-900 dark:group-hover:text-gray-100'>
{profile.content.name}
</span>
</label>
))}
{profile.content.name}
</span>
</label>
)
)}
</div>
</div>
@@ -75,45 +81,46 @@ const DataSelectorModal = ({
Custom Formats
</h4>
<span className='text-xs text-gray-500 dark:text-gray-400'>
{selectedData.customFormats.length}{' '}
selected
{customFormats.length} selected
</span>
</div>
<p className='text-xs text-gray-500 dark:text-gray-400'>
Note: Custom formats used in selected
quality profiles are automatically imported
and do't need to be selected here.
and don't need to be selected here.
</p>
</div>
<div className='grid grid-cols-2 gap-2'>
{availableData.customFormats.map(format => (
<label
key={format.file_name}
className='flex items-center p-2 bg-white dark:bg-gray-800
{(availableData?.customFormats || []).map(
format => (
<label
key={format.file_name}
className='flex items-center p-2 bg-white dark:bg-gray-800
hover:bg-gray-50 dark:hover:bg-gray-700
rounded-lg cursor-pointer group transition-colors
border border-gray-200 dark:border-gray-700'>
<input
type='checkbox'
checked={selectedData.customFormats.includes(
format.content.name
)}
onChange={() =>
onDataToggle(
'customFormats',
<input
type='checkbox'
checked={customFormats.includes(
format.content.name
)
}
className='rounded border-gray-300 text-blue-600
)}
onChange={() =>
onDataToggle(
'customFormats',
format.content.name
)
}
className='rounded border-gray-300 text-blue-600
focus:ring-blue-500 focus:ring-offset-0'
/>
<span
className='ml-3 text-sm text-gray-700 dark:text-gray-300
/>
<span
className='ml-3 text-sm text-gray-700 dark:text-gray-300
group-hover:text-gray-900 dark:group-hover:text-gray-100'>
{format.content.name}
</span>
</label>
))}
{format.content.name}
</span>
</label>
)
)}
</div>
</div>

View File

@@ -97,10 +97,12 @@ export const useArrModal = ({isOpen, onSubmit, editingArr}) => {
const validateForm = () => {
const newErrors = {};
if (formData.arrServer && !validateUrl(formData.arrServer)) {
newErrors.arrServer =
'Please enter a valid URL (e.g., http://localhost:7878)';
}
if (
formData.sync_method === 'schedule' &&
(!formData.sync_interval || formData.sync_interval < 1)
@@ -108,14 +110,18 @@ export const useArrModal = ({isOpen, onSubmit, editingArr}) => {
newErrors.sync_interval =
'Please enter a valid interval (minimum 1 minute)';
}
if (
formData.sync_method !== 'manual' &&
!formData.data_to_sync.profiles.length &&
!formData.data_to_sync.customFormats.length
) {
newErrors.data_to_sync =
'Please select at least one profile or custom format to sync';
// Safely check data_to_sync structure
if (formData.sync_method !== 'manual') {
const profiles = formData.data_to_sync?.profiles || [];
const customFormats = formData.data_to_sync?.customFormats || [];
if (profiles.length === 0 && customFormats.length === 0) {
newErrors.data_to_sync =
'Please select at least one profile or custom format to sync';
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@@ -240,28 +246,54 @@ export const useArrModal = ({isOpen, onSubmit, editingArr}) => {
const handleInputChange = e => {
const {id, value} = e.target;
setFormData(prev => ({
...prev,
[id]: id === 'sync_interval' ? parseInt(value) || 0 : value,
...(id === 'sync_method' && value === 'manual'
? {data_to_sync: {profiles: [], customFormats: []}}
: {})
}));
setFormData(prev => {
// Handle sync method changes
if (id === 'sync_method') {
return {
...prev,
[id]: value,
data_to_sync:
value === 'manual'
? {profiles: [], customFormats: []}
: prev.data_to_sync || {
profiles: [],
customFormats: []
}
};
}
// Handle other input changes
return {
...prev,
[id]: id === 'sync_interval' ? parseInt(value) || 0 : value
};
});
if (errors[id]) {
setErrors(prev => ({...prev, [id]: ''}));
}
};
const handleDataToggle = (type, name) => {
setFormData(prev => ({
...prev,
data_to_sync: {
...prev.data_to_sync,
[type]: prev.data_to_sync[type].includes(name)
? prev.data_to_sync[type].filter(item => item !== name)
: [...prev.data_to_sync[type], name]
}
}));
setFormData(prev => {
// Ensure data_to_sync exists with proper structure
const currentData = prev.data_to_sync || {
profiles: [],
customFormats: []
};
const currentArray = currentData[type] || [];
return {
...prev,
data_to_sync: {
...currentData,
[type]: currentArray.includes(name)
? currentArray.filter(item => item !== name)
: [...currentArray, name]
}
};
});
if (errors.data_to_sync) {
setErrors(prev => ({...prev, data_to_sync: ''}));
}