Type metric collections (MacroscalcMetric/MetricValue) end-to-end

This commit is contained in:
James Clifford Spratt 2026-06-27 17:20:59 +01:00
parent 6bc2364286
commit 0bfbe9934f
17 changed files with 75 additions and 69 deletions

1
__synctest.txt Normal file
View file

@ -0,0 +1 @@
synctest-1782576047

View file

@ -236,7 +236,7 @@ export class I18nManager {
// FIX: Use typed interfaces instead of 'any'
const obsidianLocale =
this.getMomentLocale() || // Use existing safe method
document.documentElement.lang ||
activeDocument.documentElement.lang ||
(this.app as ObsidianAppWithConfig).vault?.config?.userInterfaceMode ||
(this.app as ObsidianAppWithConfig).locale;

View file

@ -59,7 +59,7 @@ export class RefreshManager {
}
// Process macroscalc containers
const macroscalcElements = document.querySelectorAll('[data-macroscalc-ids]');
const macroscalcElements = activeDocument.querySelectorAll('[data-macroscalc-ids]');
macroscalcElements.forEach((el) => {
const idsAttr = el.getAttribute('data-macroscalc-ids');
if (idsAttr) {
@ -149,7 +149,7 @@ export class RefreshManager {
async refreshAllMacroscalc(): Promise<void> {
try {
// First, find all macroscalc elements in the DOM
const macroscalcElements = document.querySelectorAll('[data-macroscalc-ids]');
const macroscalcElements = activeDocument.querySelectorAll('[data-macroscalc-ids]');
if (macroscalcElements.length > 0) {
this.plugin.logger.debug(

View file

@ -97,7 +97,9 @@ export class RenameWatcher {
}
} catch (error) {
this.plugin.logger.error('Error handling file rename:', error);
this.plugin.app.workspace.trigger('notice', t('rename.error', { error: error.message }));
this.plugin.app.workspace.trigger('notice', t('rename.error', {
error: error instanceof Error ? error.message : String(error),
}));
}
}
@ -175,7 +177,9 @@ export class RenameWatcher {
}
} catch (error) {
this.plugin.logger.error('Error performing rename operation:', error);
this.plugin.app.workspace.trigger('notice', t('rename.error', { error: error.message }));
this.plugin.app.workspace.trigger('notice', t('rename.error', {
error: error instanceof Error ? error.message : String(error),
}));
}
}
@ -224,7 +228,7 @@ export class RenameWatcher {
this.plugin.logger.error('Error reverting file rename:', error);
this.plugin.app.workspace.trigger(
'notice',
t('rename.revertError', { error: error.message })
t('rename.revertError', { error: error instanceof Error ? error.message : String(error) })
);
}
}

View file

@ -540,7 +540,7 @@ export class RowRenderer {
try {
// Find the macros block ID from the current context
const macrosContainer = document.querySelector('[data-macros-id]');
const macrosContainer = activeDocument.querySelector('[data-macros-id]');
const macrosId = macrosContainer?.getAttribute('data-macros-id');
if (!macrosId) {
@ -876,7 +876,7 @@ export class RowRenderer {
const cell = tableRow.insertCell();
cell.classList.add(CLASS_NAMES.MACRO.CELL, `${macroType}-cell`);
const content = document.createElement('div');
const content = activeDocument.createElement('div');
const total = row.protein + row.fat + row.carbs;
const percentageOfFood = total > 0 ? Math.round((value / total) * 100) : 0;

View file

@ -162,7 +162,7 @@ export class TableHeader {
public initializeButtonState(): void {
try {
const sections = document.querySelectorAll('.meal-header');
const sections = activeDocument.querySelectorAll('.meal-header');
if (sections.length === 0) {
this.isCollapsed = false;

View file

@ -1,4 +1,5 @@
import MacrosPlugin from '../../main';
import { MetricValue, MacroscalcMetric } from './metrics/MetricsRegistry';
import {
formatCalories,
formatGrams,
@ -189,7 +190,7 @@ export class MacrosCalcRenderer {
const displayOptions = this.getDisplayOptions();
// Create a document fragment to batch DOM operations
const fragment = document.createDocumentFragment();
const fragment = activeDocument.createDocumentFragment();
this.el.empty();
// Load state from MacrosState
@ -455,7 +456,7 @@ export class MacrosCalcRenderer {
const metricResults = registry.calculateMetrics(metricsData, this.metricsConfigs);
// Group metrics by category
const categorizedMetrics = new Map<string, { metric: unknown; values: unknown[] }[]>();
const categorizedMetrics = new Map<string, { metric: MacroscalcMetric; values: MetricValue[] }[]>();
for (const [metricId, values] of metricResults) {
const metric = registry.get(metricId);
@ -527,7 +528,7 @@ export class MacrosCalcRenderer {
private createCustomMetricCard(
container: HTMLElement,
metricValue: unknown,
metricValue: MetricValue,
category?: string
): void {
const card = container.createDiv({
@ -1005,7 +1006,7 @@ export class MacrosCalcRenderer {
];
detailHeaderData.forEach((headerInfo) => {
const th = document.createElement('th');
const th = activeDocument.createElement('th');
const desktopSpan = createEl('span', {
cls: 'header-text-desktop',
text: headerInfo.text,
@ -1708,7 +1709,7 @@ export class MacrosCalcRenderer {
this.plugin.logger.debug('Chart created successfully:', chart);
const resizeHandler = () => {
if (document.contains(chartCanvas)) {
if (activeDocument.contains(chartCanvas)) {
chart.resize();
}
};
@ -1747,7 +1748,7 @@ export class MacrosCalcRenderer {
t('table.headers.fat'),
t('table.headers.carbs'),
].forEach((header) => {
const th = document.createElement('th');
const th = activeDocument.createElement('th');
th.textContent = header;
headerRow.appendChild(th);
});

View file

@ -107,7 +107,7 @@ export function registerMacrosCalcProcessor(plugin: MacrosPlugin): void {
// Setup cleanup
plugin.registerEvent(
plugin.app.workspace.on('layout-change', () => {
if (!el.isConnected || !document.contains(el)) {
if (!el.isConnected || !activeDocument.contains(el)) {
plugin.macroService._activeMacrosCalcRenderers.delete(renderer);
}
})
@ -131,7 +131,7 @@ async function forceRefreshAllRenderers(plugin: MacrosPlugin): Promise<void> {
plugin.logger.debug(`Force refreshing all macroscalc renderers`);
// First, find all macroscalc elements in the DOM
const macroscalcElements = document.querySelectorAll('[data-macroscalc-ids]');
const macroscalcElements = activeDocument.querySelectorAll('[data-macroscalc-ids]');
if (macroscalcElements.length > 0) {
plugin.logger.debug(`Found ${macroscalcElements.length} macroscalc elements to refresh`);
@ -193,7 +193,7 @@ async function forceRefreshAllRenderers(plugin: MacrosPlugin): Promise<void> {
for (const renderer of plugin.macroService._activeMacrosCalcRenderers) {
// Check if the renderer's element is still in the DOM
if (renderer.el && renderer.el.isConnected && document.contains(renderer.el)) {
if (renderer.el && renderer.el.isConnected && activeDocument.contains(renderer.el)) {
validRenderers.add(renderer);
}
}

View file

@ -1,6 +1,6 @@
import { Modal } from 'obsidian';
import MacrosPlugin from '../../../main';
import { MetricsRegistry, MetricConfig } from './MetricsRegistry';
import { MetricsRegistry, MetricConfig, MacroscalcMetric } from './MetricsRegistry';
import { t } from '../../../lang/I18nManager';
export class MetricsEditModal extends Modal {
@ -124,8 +124,8 @@ export class MetricsEditModal extends Modal {
return categoryNames[categoryKey] || categoryKey.toUpperCase();
}
private groupMetricsByCategory(): Record<string, unknown[]> {
const categories: Record<string, unknown[]> = {};
private groupMetricsByCategory(): Record<string, MacroscalcMetric[]> {
const categories: Record<string, MacroscalcMetric[]> = {};
this.registry.getAll().forEach((metric) => {
const category = metric.category || 'other';
@ -138,7 +138,7 @@ export class MetricsEditModal extends Modal {
return categories;
}
private renderMetricSetting(container: HTMLElement, metric: unknown): void {
private renderMetricSetting(container: HTMLElement, metric: MacroscalcMetric): void {
// Find the working config for this metric (should always exist now)
const config = this.workingConfigs.find((c) => c.id === metric.id);
if (!config) {
@ -211,7 +211,7 @@ export class MetricsEditModal extends Modal {
private updateConfigVisibility(
settingItem: HTMLElement,
enabled: boolean,
metric: unknown,
metric: MacroscalcMetric,
config: MetricConfig
): void {
// Remove existing config container

View file

@ -24,7 +24,7 @@ export class AdherenceMetric implements MacroscalcMetric {
.addSlider((slider) => {
slider
.setLimits(5, 25, 5)
.setValue(config.calorieTolerance || 10)
.setValue((config.calorieTolerance as number) || 10)
.setDynamicTooltip()
.onChange((value) => {
config.calorieTolerance = value;
@ -38,7 +38,7 @@ export class AdherenceMetric implements MacroscalcMetric {
.addSlider((slider) => {
slider
.setLimits(5, 25, 5)
.setValue(config.proteinTolerance || 10)
.setValue((config.proteinTolerance as number) || 10)
.setDynamicTooltip()
.onChange((value) => {
config.proteinTolerance = value;
@ -52,7 +52,7 @@ export class AdherenceMetric implements MacroscalcMetric {
.addSlider((slider) => {
slider
.setLimits(5, 25, 5)
.setValue(config.fatTolerance || 15)
.setValue((config.fatTolerance as number) || 15)
.setDynamicTooltip()
.onChange((value) => {
config.fatTolerance = value;
@ -66,7 +66,7 @@ export class AdherenceMetric implements MacroscalcMetric {
.addSlider((slider) => {
slider
.setLimits(5, 25, 5)
.setValue(config.carbsTolerance || 15)
.setValue((config.carbsTolerance as number) || 15)
.setDynamicTooltip()
.onChange((value) => {
config.carbsTolerance = value;
@ -100,10 +100,10 @@ export class AdherenceMetric implements MacroscalcMetric {
const adherenceConfig = data.configs.find((c) => c.id === 'adherence');
if (adherenceConfig && adherenceConfig.settings) {
config = {
calorieTolerance: adherenceConfig.settings.calorieTolerance || 10,
proteinTolerance: adherenceConfig.settings.proteinTolerance || 10,
fatTolerance: adherenceConfig.settings.fatTolerance || 15,
carbsTolerance: adherenceConfig.settings.carbsTolerance || 15,
calorieTolerance: (adherenceConfig.settings.calorieTolerance as number) || 10,
proteinTolerance: (adherenceConfig.settings.proteinTolerance as number) || 10,
fatTolerance: (adherenceConfig.settings.fatTolerance as number) || 15,
carbsTolerance: (adherenceConfig.settings.carbsTolerance as number) || 15,
};
}
}

View file

@ -163,7 +163,7 @@ class SortableTabOrder extends Component {
private setupEventListeners(): void {
this.registerDomEvent(this.listEl, 'dragstart', (e: DragEvent) => {
const target = e.target as HTMLElement;
const listItem = target.closest('.sortable-tab-item');
const listItem = target.closest('.sortable-tab-item') as HTMLElement | null;
if (listItem) {
this.draggedItem = listItem;
@ -194,7 +194,7 @@ class SortableTabOrder extends Component {
e.preventDefault();
const target = e.target as HTMLElement;
const listItem = target.closest('.sortable-tab-item');
const listItem = target.closest('.sortable-tab-item') as HTMLElement | null;
if (listItem && listItem !== this.draggedItem) {
this.listEl.querySelectorAll('.sortable-tab-item').forEach((item) => {
@ -218,7 +218,7 @@ class SortableTabOrder extends Component {
if (!this.draggedItem) return;
const target = e.target as HTMLElement;
const dropTarget = target.closest('.sortable-tab-item');
const dropTarget = target.closest('.sortable-tab-item') as HTMLElement | null;
if (dropTarget && dropTarget !== this.draggedItem) {
const dropIndex = parseInt(dropTarget.dataset.index || '0');
@ -386,7 +386,7 @@ export class NutritionalSettingTab extends PluginSettingTab {
}
// Re-render content
const contentEl = this.containerEl.querySelector('.macros-settings-content');
const contentEl = this.containerEl.querySelector('.macros-settings-content') as HTMLElement | null;
if (contentEl) {
this.renderActiveTab(contentEl);
}

View file

@ -32,21 +32,21 @@ export class ContextMenuManager {
* Hide all active tooltips before showing context menu
*/
private hideActiveTooltips(): void {
const activeTooltips = document.querySelectorAll('.macro-tooltip');
const activeTooltips = activeDocument.querySelectorAll('.macro-tooltip');
activeTooltips.forEach((tooltip) => {
(tooltip as HTMLElement).style.display = 'none';
});
document.body.classList.add('context-menu-open');
activeDocument.body.classList.add('context-menu-open');
}
/**
* Re-enable tooltips after context menu closes
*/
private restoreTooltips(): void {
document.body.classList.remove('context-menu-open');
activeDocument.body.classList.remove('context-menu-open');
const hiddenTooltips = document.querySelectorAll('.macro-tooltip');
const hiddenTooltips = activeDocument.querySelectorAll('.macro-tooltip');
hiddenTooltips.forEach((tooltip) => {
(tooltip as HTMLElement).style.display = '';
});
@ -57,7 +57,7 @@ export class ContextMenuManager {
*/
private applyMenuStyling(): void {
window.setTimeout(() => {
const menuElement = document.querySelector('.menu');
const menuElement = activeDocument.querySelector('.menu');
if (menuElement) {
menuElement.setAttribute('data-macros-plugin', 'true');
}
@ -244,7 +244,7 @@ export class ContextMenuManager {
};
window.setTimeout(() => {
if (!document.querySelector('.menu[data-macros-plugin="true"]')) {
if (!activeDocument.querySelector('.menu[data-macros-plugin="true"]')) {
this.restoreTooltips();
}
}, 100);
@ -264,7 +264,7 @@ export class ContextMenuManager {
});
});
observer.observe(document.body, { childList: true, subtree: true });
observer.observe(activeDocument.body, { childList: true, subtree: true });
window.setTimeout(() => {
observer.disconnect();
@ -403,7 +403,7 @@ export class ContextMenuManager {
};
window.setTimeout(() => {
if (!document.querySelector('.menu[data-macros-plugin="true"]')) {
if (!activeDocument.querySelector('.menu[data-macros-plugin="true"]')) {
this.restoreTooltips();
}
}, 100);
@ -423,7 +423,7 @@ export class ContextMenuManager {
});
});
observer.observe(document.body, { childList: true, subtree: true });
observer.observe(activeDocument.body, { childList: true, subtree: true });
window.setTimeout(() => {
observer.disconnect();
@ -589,7 +589,7 @@ export class ContextMenuManager {
};
window.setTimeout(() => {
if (!document.querySelector('.menu[data-macros-plugin="true"]')) {
if (!activeDocument.querySelector('.menu[data-macros-plugin="true"]')) {
this.restoreTooltips();
}
}, 100);
@ -609,7 +609,7 @@ export class ContextMenuManager {
});
});
observer.observe(document.body, { childList: true, subtree: true });
observer.observe(activeDocument.body, { childList: true, subtree: true });
window.setTimeout(() => {
observer.disconnect();

View file

@ -42,7 +42,7 @@ export class FoodResultsModal extends Modal {
this.loadPage(0);
// Add document-level event handling - register with component for cleanup
this.component.registerDomEvent(document, 'keydown', this.handleKeyNav);
this.component.registerDomEvent(activeDocument, 'keydown', this.handleKeyNav);
}
onClose() {

View file

@ -217,7 +217,7 @@ export class RenameConfirmationModal extends Modal {
}
private addStyles(): void {
const styleEl = document.createElement('style');
const styleEl = activeDocument.createElement('style');
styleEl.textContent = `
.rename-summary {
margin-bottom: 1rem;
@ -311,11 +311,11 @@ export class RenameConfirmationModal extends Modal {
border-top: 1px solid var(--background-modifier-border);
}
`;
document.head.appendChild(styleEl);
activeDocument.head.appendChild(styleEl);
// Clean up styles when modal closes
this.onClose = () => {
document.head.removeChild(styleEl);
activeDocument.head.removeChild(styleEl);
};
}

View file

@ -170,7 +170,7 @@ export class BarcodeScanner extends Component {
if (!this.video || !this.codeReader) return;
if (!this.canvas) {
this.canvas = document.createElement('canvas');
this.canvas = activeDocument.createElement('canvas');
this.context = this.canvas.getContext('2d', { willReadFrequently: true });
}
@ -492,7 +492,7 @@ export class BarcodeScanner extends Component {
} = {}
): Promise<BarcodeResult | null> {
// Create optimized canvas with willReadFrequently flag to suppress warnings
const canvas = document.createElement('canvas');
const canvas = activeDocument.createElement('canvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true, // This fixes the Canvas2D warnings
});
@ -739,7 +739,7 @@ export class BarcodeScannerModal extends Modal {
cls: 'barcode-scan-upload-btn',
});
this.uploadInput = document.createElement('input');
this.uploadInput = activeDocument.createElement('input');
this.uploadInput.type = 'file';
this.uploadInput.accept = 'image/*';
this.uploadInput.capture = 'environment';

View file

@ -73,12 +73,12 @@ export class ZXingLoader {
async (): Promise<ZXingLibrary> => {
return new Promise((resolve, reject) => {
// Remove any existing script first
const existingScript = document.querySelector('script[src*="zxing"]');
const existingScript = activeDocument.querySelector('script[src*="zxing"]');
if (existingScript) {
existingScript.remove();
}
const script = document.createElement('script');
const script = activeDocument.createElement('script');
script.src = 'https://unpkg.com/@zxing/browser@0.1.1/lib/index.min.js';
script.onload = () => {
// Check multiple possible global names
@ -92,7 +92,7 @@ export class ZXingLoader {
script.onerror = (_error) => {
reject(new Error('Failed to load ZXing browser from CDN'));
};
document.head.appendChild(script);
activeDocument.head.appendChild(script);
});
},
@ -100,12 +100,12 @@ export class ZXingLoader {
async (): Promise<ZXingLibrary> => {
return new Promise((resolve, reject) => {
// Remove any existing script first
const existingScript = document.querySelector('script[src*="zxing"]');
const existingScript = activeDocument.querySelector('script[src*="zxing"]');
if (existingScript) {
existingScript.remove();
}
const script = document.createElement('script');
const script = activeDocument.createElement('script');
script.src = 'https://unpkg.com/@zxing/library@0.20.0/umd/index.min.js';
script.onload = () => {
// Check multiple possible global names
@ -119,14 +119,14 @@ export class ZXingLoader {
script.onerror = (_error) => {
reject(new Error('Failed to load ZXing from CDN'));
};
document.head.appendChild(script);
activeDocument.head.appendChild(script);
});
},
// Method 5: Try alternative CDN
async (): Promise<ZXingLibrary> => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const script = activeDocument.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@zxing/library@0.20.0/umd/index.min.js';
script.onload = () => {
const ZXing = window.ZXing || window.ZXingLibrary;
@ -137,7 +137,7 @@ export class ZXingLoader {
}
};
script.onerror = () => reject(new Error('Failed to load ZXing from jsdelivr'));
document.head.appendChild(script);
activeDocument.head.appendChild(script);
});
},
];
@ -244,7 +244,7 @@ export class ZXingLoader {
// Add decodeFromImageData method
reader.decodeFromImageData = async function (imageData: ImageData): Promise<ZXingResult> {
const canvas = document.createElement('canvas');
const canvas = activeDocument.createElement('canvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext('2d');
@ -261,7 +261,7 @@ export class ZXingLoader {
// Add decodeFromImageElement method
reader.decodeFromImageElement = async function (img: HTMLImageElement): Promise<ZXingResult> {
const canvas = document.createElement('canvas');
const canvas = activeDocument.createElement('canvas');
canvas.width = img.width || img.naturalWidth;
canvas.height = img.height || img.naturalHeight;
const ctx = canvas.getContext('2d');

View file

@ -14,10 +14,10 @@ function detectMobileDevice(): boolean {
function ensureTooltipEl(): HTMLDivElement {
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl = activeDocument.createElement('div');
tooltipEl.className = 'macro-tooltip macro-tooltip-hidden';
tooltipEl.setAttribute('style', '--x: -9999px; --y: -9999px;');
document.body.appendChild(tooltipEl);
activeDocument.body.appendChild(tooltipEl);
}
return tooltipEl;
}
@ -59,8 +59,8 @@ export class TooltipManager {
});
// Global event listeners
plugin.registerDomEvent(document, 'visibilitychange', () => {
if (document.hidden) TooltipManager.forceHide();
plugin.registerDomEvent(activeDocument, 'visibilitychange', () => {
if (activeDocument.hidden) TooltipManager.forceHide();
});
plugin.registerDomEvent(window, 'resize', () => {
@ -93,7 +93,7 @@ export class TooltipManager {
TooltipManager.forceHide();
});
const root = document.querySelector('.workspace') || document.body;
const root = activeDocument.querySelector('.workspace') || activeDocument.body;
mutationObserver.observe(root, { childList: true, subtree: true });
}
@ -171,7 +171,7 @@ export class TooltipManager {
el.classList.add('macro-tooltip-mobile');
// Position near the bottom of the Obsidian app, but above the toolbar
const obsidianApp = document.querySelector('.app-container') || document.body;
const obsidianApp = activeDocument.querySelector('.app-container') || activeDocument.body;
const appRect = obsidianApp.getBoundingClientRect();
// Set Y position to be above the bottom toolbar (more clearance)