Skip to content

Commit 0c2d659

Browse files
committed
fix issues with groups
1 parent 8c6727a commit 0c2d659

File tree

3 files changed

+196
-62
lines changed

3 files changed

+196
-62
lines changed

backend/src/routes/config.route.ts

Lines changed: 173 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -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

frontend/src/components/dashboard/DashboardGrid.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ const customCollisionDetection = (args: any) => {
4747
args.active.data.current?.type === ITEM_TYPE.APP_SHORTCUT ||
4848
args.active.data.current?.type === ITEM_TYPE.BLANK_APP;
4949

50+
const isGroupWidgetType = args.active.data.current?.type === 'group-widget';
51+
5052
// If the active item is an app shortcut, use expanded collision detection for ALL containers
5153
if (isAppShortcutType) {
5254

@@ -154,6 +156,26 @@ const customCollisionDetection = (args: any) => {
154156
}
155157
}
156158

159+
// For group widgets being dragged, use closestCenter for better reordering
160+
// but exclude group-internal droppable containers to prevent interference
161+
if (isGroupWidgetType) {
162+
// Filter out group-internal containers that shouldn't be targets for group reordering
163+
const filteredContainers = args.droppableContainers.filter((container: any) => {
164+
const containerId = container.id.toString();
165+
// Exclude group-internal droppable containers
166+
return !containerId.includes('group-droppable') &&
167+
!containerId.includes('group-widget-droppable') &&
168+
container.data.current?.type !== 'group-container' &&
169+
container.data.current?.type !== 'group-widget-container';
170+
});
171+
172+
// Use closestCenter with filtered containers for group widget reordering
173+
return closestCenter({
174+
...args,
175+
droppableContainers: filteredContainers
176+
});
177+
}
178+
157179
// Fall back to standard detection for other cases
158180
return closestCorners(args);
159181
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lab-dash",
3-
"version": "1.1.8",
3+
"version": "1.1.8-beta2",
44
"description": "This is an open-source user interface designed to manage your server and homelab",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)