@@ -294,88 +294,200 @@ const mergeSensitiveData = (newConfig: any, existingConfig: any): any => {
294294 } ;
295295
296296 const mergeItems = ( newItems : any [ ] , existingItems : any [ ] ) => {
297- return newItems . map ( newItem => {
298- // First try to find in the same layout array (for normal updates)
299- let existingItem = existingItems . find ( item => item . id === newItem . id ) ;
297+ const result : any [ ] = [ ] ;
298+ const newItemsMap = new Map ( newItems . map ( item => [ item . id , item ] ) ) ;
299+ const existingItemsMap = new Map ( existingItems . map ( item => [ item . id , item ] ) ) ;
300+ const processedIds = new Set < string > ( ) ;
301+
302+ // Check if this is a reorder operation (all existing items are present in new items)
303+ const existingIds = new Set ( existingItems . map ( item => item . id ) ) ;
304+ const newIds = new Set ( newItems . map ( item => item . id ) ) ;
305+ const isReorderOperation = existingIds . size === newIds . size &&
306+ [ ...existingIds ] . every ( id => newIds . has ( id ) ) ;
307+
308+ if ( isReorderOperation ) {
309+ // For reorder operations, respect the new order from frontend
310+ for ( const newItem of newItems ) {
311+ const existingItem = existingItemsMap . get ( newItem . id ) ;
312+ processedIds . add ( newItem . id ) ;
313+
314+ if ( existingItem ?. config && newItem . config ) {
315+ const mergedItemConfig = { ...newItem . config } ;
316+
317+ // Restore Pi-hole sensitive data if not being updated
318+ if ( newItem . config . _hasApiToken && ! newItem . config . apiToken && existingItem . config . apiToken ) {
319+ mergedItemConfig . apiToken = existingItem . config . apiToken ;
320+ }
321+ if ( newItem . config . _hasPassword && ! newItem . config . password && existingItem . config . password ) {
322+ mergedItemConfig . password = existingItem . config . password ;
323+ }
300324
301- // If not found in same layout, search across all layouts (for moved items)
302- if ( ! existingItem ) {
303- existingItem = findItemById ( newItem . id ) ;
304- }
325+ // Handle torrent client sensitive data
326+ if ( newItem . type === 'torrent-client' ) {
327+ // Restore password if not being updated
328+ if ( newItem . config . _hasPassword && ! newItem . config . password && existingItem . config . password ) {
329+ mergedItemConfig . password = existingItem . config . password ;
330+ }
331+ // Always preserve _hasPassword flag if existing item has a password
332+ if ( existingItem . config . password || existingItem . config . _hasPassword ) {
333+ mergedItemConfig . _hasPassword = true ;
334+ }
335+ }
305336
306- if ( existingItem ?. config && newItem . config ) {
307- const mergedItemConfig = { ...newItem . config } ;
337+ // Handle dual widget sensitive data
338+ if ( newItem . type === 'dual-widget' ) {
339+ if ( mergedItemConfig . topWidget ?. config && existingItem . config . topWidget ?. config ) {
340+ if ( mergedItemConfig . topWidget . config . _hasApiToken && ! mergedItemConfig . topWidget . config . apiToken && existingItem . config . topWidget . config . apiToken ) {
341+ mergedItemConfig . topWidget . config . apiToken = existingItem . config . topWidget . config . apiToken ;
342+ }
343+ if ( mergedItemConfig . topWidget . config . _hasPassword && ! mergedItemConfig . topWidget . config . password && existingItem . config . topWidget . config . password ) {
344+ mergedItemConfig . topWidget . config . password = existingItem . config . topWidget . config . password ;
345+ }
346+ }
347+ if ( mergedItemConfig . bottomWidget ?. config && existingItem . config . bottomWidget ?. config ) {
348+ if ( mergedItemConfig . bottomWidget . config . _hasApiToken && ! mergedItemConfig . bottomWidget . config . apiToken && existingItem . config . bottomWidget . config . apiToken ) {
349+ mergedItemConfig . bottomWidget . config . apiToken = existingItem . config . bottomWidget . config . apiToken ;
350+ }
351+ if ( mergedItemConfig . bottomWidget . config . _hasPassword && ! mergedItemConfig . bottomWidget . config . password && existingItem . config . bottomWidget . config . password ) {
352+ mergedItemConfig . bottomWidget . config . password = existingItem . config . bottomWidget . config . password ;
353+ }
354+ }
355+ }
308356
309- // Restore Pi-hole sensitive data if not being updated
310- if ( newItem . config . _hasApiToken && ! newItem . config . apiToken && existingItem . config . apiToken ) {
311- mergedItemConfig . apiToken = existingItem . config . apiToken ;
312- }
313- if ( newItem . config . _hasPassword && ! newItem . config . password && existingItem . config . password ) {
314- mergedItemConfig . password = existingItem . config . password ;
315- }
357+ // Handle group widget items
358+ if ( newItem . type === 'group-widget' && existingItem . config . items ) {
359+ // If the new item has items, merge them; otherwise, preserve existing items
360+ if ( mergedItemConfig . items && mergedItemConfig . items . length > 0 ) {
361+ mergedItemConfig . items = mergeItems ( mergedItemConfig . items , existingItem . config . items ) ;
362+ } else {
363+ // If new item has no items or empty array, preserve existing items
364+ mergedItemConfig . items = existingItem . config . items ;
365+ }
366+ }
316367
317- // Handle torrent client sensitive data
318- if ( newItem . type === 'torrent-client' ) {
319- // Restore password if not being updated
320- if ( newItem . config . _hasPassword && ! newItem . config . password && existingItem . config . password ) {
321- mergedItemConfig . password = existingItem . config . password ;
368+ // Clean up flags (but preserve torrent client flags)
369+ if ( newItem . type !== 'torrent-client' ) {
370+ delete mergedItemConfig . _hasApiToken ;
371+ delete mergedItemConfig . _hasPassword ;
322372 }
323- // Always preserve _hasPassword flag if existing item has a password
324- if ( existingItem . config . password || existingItem . config . _hasPassword ) {
325- mergedItemConfig . _hasPassword = true ;
373+ // For torrent clients, never delete security flags - they should always be preserved
374+ if ( mergedItemConfig . topWidget ?. config ) {
375+ delete mergedItemConfig . topWidget . config . _hasApiToken ;
376+ delete mergedItemConfig . topWidget . config . _hasPassword ;
377+ }
378+ if ( mergedItemConfig . bottomWidget ?. config ) {
379+ delete mergedItemConfig . bottomWidget . config . _hasApiToken ;
380+ delete mergedItemConfig . bottomWidget . config . _hasPassword ;
326381 }
327382
383+ result . push ( { ...newItem , config : mergedItemConfig } ) ;
384+ } else {
385+ result . push ( newItem ) ;
328386 }
329-
330- // Handle dual widget sensitive data
331- if ( newItem . type === 'dual-widget' ) {
332-
333- if ( mergedItemConfig . topWidget ?. config && existingItem . config . topWidget ?. config ) {
334- if ( mergedItemConfig . topWidget . config . _hasApiToken && ! mergedItemConfig . topWidget . config . apiToken && existingItem . config . topWidget . config . apiToken ) {
335- mergedItemConfig . topWidget . config . apiToken = existingItem . config . topWidget . config . apiToken ;
387+ }
388+ } else {
389+ // For non-reorder operations, preserve existing order and add new items
390+ // First, preserve all existing items in their original order
391+ // Update them with new data if available, otherwise keep as-is
392+ for ( const existingItem of existingItems ) {
393+ const newItem = newItemsMap . get ( existingItem . id ) ;
394+
395+ if ( newItem ) {
396+ // This item exists in both old and new - merge sensitive data
397+ processedIds . add ( existingItem . id ) ;
398+
399+ if ( existingItem ?. config && newItem . config ) {
400+ const mergedItemConfig = { ...newItem . config } ;
401+
402+ // Restore Pi-hole sensitive data if not being updated
403+ if ( newItem . config . _hasApiToken && ! newItem . config . apiToken && existingItem . config . apiToken ) {
404+ mergedItemConfig . apiToken = existingItem . config . apiToken ;
405+ }
406+ if ( newItem . config . _hasPassword && ! newItem . config . password && existingItem . config . password ) {
407+ mergedItemConfig . password = existingItem . config . password ;
336408 }
337- if ( mergedItemConfig . topWidget . config . _hasPassword && ! mergedItemConfig . topWidget . config . password && existingItem . config . topWidget . config . password ) {
338- mergedItemConfig . topWidget . config . password = existingItem . config . topWidget . config . password ;
339409
410+ // Handle torrent client sensitive data
411+ if ( newItem . type === 'torrent-client' ) {
412+ // Restore password if not being updated
413+ if ( newItem . config . _hasPassword && ! newItem . config . password && existingItem . config . password ) {
414+ mergedItemConfig . password = existingItem . config . password ;
415+ }
416+ // Always preserve _hasPassword flag if existing item has a password
417+ if ( existingItem . config . password || existingItem . config . _hasPassword ) {
418+ mergedItemConfig . _hasPassword = true ;
419+ }
340420 }
341- }
342- if ( mergedItemConfig . bottomWidget ?. config && existingItem . config . bottomWidget ?. config ) {
343421
344- if ( mergedItemConfig . bottomWidget . config . _hasApiToken && ! mergedItemConfig . bottomWidget . config . apiToken && existingItem . config . bottomWidget . config . apiToken ) {
345- mergedItemConfig . bottomWidget . config . apiToken = existingItem . config . bottomWidget . config . apiToken ;
422+ // Handle dual widget sensitive data
423+ if ( newItem . type === 'dual-widget' ) {
424+ if ( mergedItemConfig . topWidget ?. config && existingItem . config . topWidget ?. config ) {
425+ if ( mergedItemConfig . topWidget . config . _hasApiToken && ! mergedItemConfig . topWidget . config . apiToken && existingItem . config . topWidget . config . apiToken ) {
426+ mergedItemConfig . topWidget . config . apiToken = existingItem . config . topWidget . config . apiToken ;
427+ }
428+ if ( mergedItemConfig . topWidget . config . _hasPassword && ! mergedItemConfig . topWidget . config . password && existingItem . config . topWidget . config . password ) {
429+ mergedItemConfig . topWidget . config . password = existingItem . config . topWidget . config . password ;
430+ }
431+ }
432+ if ( mergedItemConfig . bottomWidget ?. config && existingItem . config . bottomWidget ?. config ) {
433+ if ( mergedItemConfig . bottomWidget . config . _hasApiToken && ! mergedItemConfig . bottomWidget . config . apiToken && existingItem . config . bottomWidget . config . apiToken ) {
434+ mergedItemConfig . bottomWidget . config . apiToken = existingItem . config . bottomWidget . config . apiToken ;
435+ }
436+ if ( mergedItemConfig . bottomWidget . config . _hasPassword && ! mergedItemConfig . bottomWidget . config . password && existingItem . config . bottomWidget . config . password ) {
437+ mergedItemConfig . bottomWidget . config . password = existingItem . config . bottomWidget . config . password ;
438+ }
439+ }
440+ }
346441
442+ // Handle group widget items
443+ if ( newItem . type === 'group-widget' && existingItem . config . items ) {
444+ // If the new item has items, merge them; otherwise, preserve existing items
445+ if ( mergedItemConfig . items && mergedItemConfig . items . length > 0 ) {
446+ mergedItemConfig . items = mergeItems ( mergedItemConfig . items , existingItem . config . items ) ;
447+ } else {
448+ // If new item has no items or empty array, preserve existing items
449+ mergedItemConfig . items = existingItem . config . items ;
450+ }
347451 }
348- if ( mergedItemConfig . bottomWidget . config . _hasPassword && ! mergedItemConfig . bottomWidget . config . password && existingItem . config . bottomWidget . config . password ) {
349- mergedItemConfig . bottomWidget . config . password = existingItem . config . bottomWidget . config . password ;
350452
453+ // Clean up flags (but preserve torrent client flags)
454+ if ( newItem . type !== 'torrent-client' ) {
455+ delete mergedItemConfig . _hasApiToken ;
456+ delete mergedItemConfig . _hasPassword ;
457+ }
458+ // For torrent clients, never delete security flags - they should always be preserved
459+ if ( mergedItemConfig . topWidget ?. config ) {
460+ delete mergedItemConfig . topWidget . config . _hasApiToken ;
461+ delete mergedItemConfig . topWidget . config . _hasPassword ;
462+ }
463+ if ( mergedItemConfig . bottomWidget ?. config ) {
464+ delete mergedItemConfig . bottomWidget . config . _hasApiToken ;
465+ delete mergedItemConfig . bottomWidget . config . _hasPassword ;
351466 }
352- }
353- }
354467
355- // Handle group widget items
356- if ( newItem . type === 'group-widget' && mergedItemConfig . items && existingItem . config . items ) {
357- mergedItemConfig . items = mergeItems ( mergedItemConfig . items , existingItem . config . items ) ;
468+ result . push ( { ...newItem , config : mergedItemConfig } ) ;
469+ } else {
470+ result . push ( newItem ) ;
471+ }
358472 }
473+ // Note: Removed the else clause that was adding back deleted items
474+ // If an item exists in old but not in new, it means it was deleted - don't add it back
475+ }
359476
360- // Clean up flags (but preserve torrent client flags)
361- if ( newItem . type !== 'torrent-client' ) {
362- delete mergedItemConfig . _hasApiToken ;
363- delete mergedItemConfig . _hasPassword ;
364- }
365- // For torrent clients, never delete security flags - they should always be preserved
366- if ( mergedItemConfig . topWidget ?. config ) {
367- delete mergedItemConfig . topWidget . config . _hasApiToken ;
368- delete mergedItemConfig . topWidget . config . _hasPassword ;
369- }
370- if ( mergedItemConfig . bottomWidget ?. config ) {
371- delete mergedItemConfig . bottomWidget . config . _hasApiToken ;
372- delete mergedItemConfig . bottomWidget . config . _hasPassword ;
477+ // Then, add any truly new items to the end
478+ for ( const newItem of newItems ) {
479+ if ( ! processedIds . has ( newItem . id ) ) {
480+ // This is a new item - check if it exists anywhere else in the config
481+ const foundAnywhere = findItemById ( newItem . id ) ;
482+ if ( ! foundAnywhere ) {
483+ // Truly new item - add to the end
484+ result . push ( newItem ) ;
485+ }
373486 }
374-
375- return { ...newItem , config : mergedItemConfig } ;
376487 }
377- return newItem ;
378- } ) ;
488+ }
489+
490+ return result ;
379491 } ;
380492
381493 // Merge desktop and mobile layouts
0 commit comments