feat(dashboard): implement bidirectional tracker-widget sync with re-addition

- Add previousTrackerConfig tracking in dashboardManager
- Implement detectConfigChanges() to detect disabled→enabled transitions
- Implement addEnabledWidgets() with smart tab placement using WIDGET_TO_TAB_MAP
- Update onTrackerConfigChanged() to handle both removal and re-addition
- Auto-layout affected tabs after widget changes
- Re-render current tab when widgets are added to show them immediately
- Prevent duplicate widgets per tab with type-based checking

Closes the bidirectional sync loop - widgets now automatically:
1. Disappear when fields are disabled in tracker settings
2. Reappear in appropriate tabs when fields are re-enabled
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-11-02 14:54:55 +11:00
parent 339413a6fa
commit bf44949624
+200 -7
View File
@@ -76,6 +76,7 @@ export class DashboardManager {
this.currentTabId = null;
this.widgets = new Map(); // widgetId => { widget data, element, tab }
this.defaultLayout = null;
this.previousTrackerConfig = null; // For detecting config changes
// Dashboard data structure (for TabManager)
this.dashboard = {
@@ -1670,6 +1671,169 @@ export class DashboardManager {
});
}
/**
* Widget-to-tab mapping for smart widget placement
* Maps widget types to their preferred tab IDs
*/
static WIDGET_TO_TAB_MAP = {
'calendar': 'tab-scene',
'weather': 'tab-scene',
'temperature': 'tab-scene',
'clock': 'tab-scene',
'location': 'tab-scene',
'presentCharacters': 'tab-scene',
'userStats': 'tab-status',
'userInfo': 'tab-status',
'userMood': 'tab-status',
'userAttributes': 'tab-status',
'inventory': 'tab-inventory',
'quests': 'tab-quests'
};
/**
* Detect config changes between old and new tracker configs
* Identifies fields that transitioned from disabled to enabled
* @param {Object} oldConfig - Previous tracker configuration
* @param {Object} newConfig - New tracker configuration
* @returns {Array<string>} Array of widget types that should be re-added
*/
detectConfigChanges(oldConfig, newConfig) {
if (!oldConfig) {
// First run, no changes to detect
return [];
}
const widgetsToAdd = [];
// Check infoBox widgets (calendar, weather, temperature, clock, location)
const infoBoxWidgetMap = {
'date': 'calendar',
'weather': 'weather',
'temperature': 'temperature',
'time': 'clock',
'location': 'location'
};
Object.entries(infoBoxWidgetMap).forEach(([fieldKey, widgetType]) => {
const wasDisabled = oldConfig.infoBox?.widgets?.[fieldKey]?.enabled === false;
const isNowEnabled = newConfig.infoBox?.widgets?.[fieldKey]?.enabled !== false;
if (wasDisabled && isNowEnabled) {
widgetsToAdd.push(widgetType);
console.log(`[DashboardManager] Detected re-enabled field: ${fieldKey} → widget: ${widgetType}`);
}
});
// Check userStats widget (enabled when at least one stat is enabled)
const oldStatsEnabled = oldConfig.userStats?.customStats?.filter(s => s.enabled).length > 0;
const newStatsEnabled = newConfig.userStats?.customStats?.filter(s => s.enabled).length > 0;
if (!oldStatsEnabled && newStatsEnabled) {
widgetsToAdd.push('userStats');
console.log('[DashboardManager] Detected re-enabled userStats widget');
}
// Check presentCharacters widget
const wasThoughtsDisabled = oldConfig.presentCharacters?.thoughts?.enabled === false;
const isThoughtsEnabled = newConfig.presentCharacters?.thoughts?.enabled !== false;
if (wasThoughtsDisabled && isThoughtsEnabled) {
widgetsToAdd.push('presentCharacters');
console.log('[DashboardManager] Detected re-enabled presentCharacters widget');
}
return widgetsToAdd;
}
/**
* Add widgets that were re-enabled in tracker config
* @param {Array<string>} widgetTypes - Array of widget types to add
*/
addEnabledWidgets(widgetTypes) {
if (widgetTypes.length === 0) {
return;
}
console.log(`[DashboardManager] Adding ${widgetTypes.length} re-enabled widgets:`, widgetTypes);
const addedWidgets = [];
widgetTypes.forEach(widgetType => {
// Get widget definition
const definition = this.registry.get(widgetType);
if (!definition) {
console.warn(`[DashboardManager] Widget type "${widgetType}" not found in registry`);
return;
}
// Determine target tab using mapping
const preferredTabId = DashboardManager.WIDGET_TO_TAB_MAP[widgetType] || 'tab-status';
const targetTab = this.tabManager.getTab(preferredTabId);
// Fallback to first tab if preferred tab doesn't exist
const tab = targetTab || this.dashboard.tabs[0];
if (!tab) {
console.warn(`[DashboardManager] No tab available to add widget ${widgetType}`);
return;
}
// Check for duplicates - don't add if widget type already exists in this tab
const alreadyExists = tab.widgets?.some(w => w.type === widgetType);
if (alreadyExists) {
console.log(`[DashboardManager] Widget ${widgetType} already exists in tab ${tab.id}, skipping`);
return;
}
// Generate unique widget ID
const widgetId = `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Find available position in the target tab
const position = this.findAvailablePositionInWidgets(
definition.defaultSize,
tab.widgets || []
);
// Create widget data
const widget = {
id: widgetId,
type: widgetType,
x: position.x,
y: position.y,
w: definition.defaultSize.w,
h: definition.defaultSize.h,
config: {}
};
// Add to tab
if (!tab.widgets) {
tab.widgets = [];
}
tab.widgets.push(widget);
console.log(`[DashboardManager] Added widget ${widgetType} (${widgetId}) to tab ${tab.id} at (${position.x}, ${position.y})`);
addedWidgets.push({
widgetId,
widgetType,
tabId: tab.id
});
});
// Auto-layout affected tabs to optimize positioning
if (addedWidgets.length > 0) {
const affectedTabs = new Set(addedWidgets.map(w => w.tabId));
affectedTabs.forEach(tabId => {
const tab = this.tabManager.getTab(tabId);
if (tab && tab.widgets && tab.widgets.length > 0) {
console.log(`[DashboardManager] Auto-layouting tab ${tabId} after widget addition`);
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
}
});
}
console.log(`[DashboardManager] Added ${addedWidgets.length} widgets`);
}
/**
* Handle tracker configuration changes from editor
* Removes disabled widgets and refreshes remaining widgets
@@ -1678,25 +1842,54 @@ export class DashboardManager {
onTrackerConfigChanged(config) {
console.log('[DashboardManager] Processing tracker config changes...');
// Step 1: Remove widgets that are now disabled
// Step 1: Detect config changes (disabled → enabled)
const widgetsToAdd = this.detectConfigChanges(this.previousTrackerConfig, config);
// Step 2: Remove widgets that are now disabled
const removedWidgets = this.removeDisabledWidgets(config);
// Step 2: If widgets were removed, auto-layout affected tabs
// Step 3: Add widgets that were re-enabled
this.addEnabledWidgets(widgetsToAdd);
// Step 4: If widgets were removed or added, auto-layout affected tabs
const allAffectedTabs = new Set([
...removedWidgets.map(w => w.tabId),
// Note: addEnabledWidgets already handles auto-layout for added widgets
]);
if (removedWidgets.length > 0) {
const affectedTabs = new Set(removedWidgets.map(w => w.tabId));
affectedTabs.forEach(tabId => {
allAffectedTabs.forEach(tabId => {
const tab = this.tabManager.getTab(tabId);
if (tab && tab.widgets && tab.widgets.length > 0) {
console.log(`[DashboardManager] Auto-layouting tab ${tabId} after widget removal`);
console.log(`[DashboardManager] Auto-layouting tab ${tabId} after changes`);
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
}
});
}
// Step 3: Refresh all remaining widgets (re-render with new config)
// Step 5: Refresh all widgets (re-render with new config)
// This updates widget content (e.g., renamed stats) without repositioning
this.refreshAllWidgets();
// Step 4: Save layout changes
// Step 6: If widgets were added to current tab, re-render to show them
if (widgetsToAdd.length > 0) {
const currentTab = this.tabManager.getTab(this.currentTabId);
if (currentTab) {
// Re-render current tab to show newly added widgets
this.clearGrid();
currentTab.widgets.forEach(widget => {
const definition = this.registry.get(widget.type);
if (definition) {
this.renderWidget(widget, definition);
}
});
}
}
// Step 7: Store current config for next comparison
this.previousTrackerConfig = JSON.parse(JSON.stringify(config)); // Deep clone
// Step 8: Save layout changes
this.triggerAutoSave();
console.log('[DashboardManager] Tracker config refresh complete');