mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
fix: ensure data_to_sync has a safe structure and improve sync method handling
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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: ''}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user