mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: add performance warning for large datasets (>1000 tasks)
- Add modal warning dialog with Lucide icons when queries return >1000 tasks - Users can choose to cancel or continue with full load - Prevents accidental UI freezing on very large datasets - Uses proper DOM creation (no innerHTML) and static imports - Shows informative fallback message when loading is cancelled - Applied to both list and kanban views Addresses performance concerns while giving users informed choice.
This commit is contained in:
parent
31d906ea4e
commit
ccd0011a7f
3 changed files with 268 additions and 2 deletions
|
|
@ -5,9 +5,10 @@
|
|||
* It handles formula computation, data transformation, and rendering of TaskNotes items in Bases views.
|
||||
*/
|
||||
import TaskNotesPlugin from '../main';
|
||||
import { BasesDataItem, identifyTaskNotesFromBasesData, getBasesVisibleProperties } from './helpers';
|
||||
import { BasesDataItem, identifyTaskNotesFromBasesData, getBasesVisibleProperties, showPerformanceWarning } from './helpers';
|
||||
import { TaskInfo } from '../types';
|
||||
import { createTaskCard, updateTaskCard } from '../ui/TaskCard';
|
||||
import { setIcon } from 'obsidian';
|
||||
|
||||
export interface BasesContainerLike {
|
||||
results?: Map<any, any>;
|
||||
|
|
@ -20,6 +21,7 @@ export interface ViewConfig {
|
|||
errorPrefix: string;
|
||||
}
|
||||
|
||||
|
||||
export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: ViewConfig) {
|
||||
return function tasknotesBaseViewFactory(basesContainer: BasesContainerLike) {
|
||||
let currentRoot: HTMLElement | null = null;
|
||||
|
|
@ -122,6 +124,47 @@ export function buildTasknotesBaseViewFactory(plugin: TaskNotesPlugin, config: V
|
|||
|
||||
if (renderCancellation.cancelled) return;
|
||||
|
||||
// Performance warning for large datasets
|
||||
if (taskNotes.length > 1000) {
|
||||
const shouldContinue = await showPerformanceWarning(taskNotes.length);
|
||||
if (!shouldContinue || renderCancellation.cancelled) {
|
||||
// Show partial message
|
||||
itemsContainer.innerHTML = '';
|
||||
const partialEl = document.createElement('div');
|
||||
partialEl.className = 'tn-bases-partial';
|
||||
partialEl.style.cssText = 'padding: 20px; text-align: center; color: var(--text-muted);';
|
||||
|
||||
// Create icon and title
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.style.cssText = 'margin-bottom: 8px; display: flex; align-items: center; justify-content: center; gap: 8px;';
|
||||
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.style.cssText = 'color: var(--text-warning); display: flex; align-items: center;';
|
||||
setIcon(iconEl, 'alert-triangle');
|
||||
titleDiv.appendChild(iconEl);
|
||||
|
||||
const titleText = document.createElement('span');
|
||||
titleText.textContent = 'Performance limit reached';
|
||||
titleDiv.appendChild(titleText);
|
||||
partialEl.appendChild(titleDiv);
|
||||
|
||||
// Create description
|
||||
const description = document.createElement('div');
|
||||
description.style.cssText = 'font-size: 0.9em;';
|
||||
description.textContent = `Found ${taskNotes.length} tasks, but loading was cancelled to prevent performance issues.`;
|
||||
partialEl.appendChild(description);
|
||||
|
||||
// Create advice
|
||||
const advice = document.createElement('div');
|
||||
advice.style.cssText = 'font-size: 0.9em; margin-top: 8px;';
|
||||
advice.textContent = 'Consider refining your query to show fewer results.';
|
||||
partialEl.appendChild(advice);
|
||||
|
||||
itemsContainer.appendChild(partialEl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskNotes.length === 0) {
|
||||
itemsContainer.innerHTML = '';
|
||||
const emptyEl = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import TaskNotesPlugin from '../main';
|
||||
import { TaskInfo } from '../types';
|
||||
import { setIcon } from 'obsidian';
|
||||
|
||||
export interface BasesDataItem {
|
||||
key?: string;
|
||||
|
|
@ -314,6 +315,185 @@ async function renderAllTasksAsync(
|
|||
/**
|
||||
* Render a raw Bases data item for debugging/inspection
|
||||
*/
|
||||
/**
|
||||
* Show performance warning dialog for large datasets
|
||||
*/
|
||||
export async function showPerformanceWarning(taskCount: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
// Create modal overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'tn-performance-warning-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(2px);
|
||||
`;
|
||||
|
||||
// Create modal content
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'tn-performance-warning-modal';
|
||||
modal.style.cssText = `
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
max-width: 480px;
|
||||
margin: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
// Warning icon and title
|
||||
const header = document.createElement('div');
|
||||
header.style.cssText = 'display: flex; align-items: center; margin-bottom: 16px;';
|
||||
|
||||
// Create warning icon using Lucide
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.style.cssText = 'margin-right: 12px; color: var(--text-warning); display: flex; align-items: center;';
|
||||
setIcon(iconEl, 'alert-triangle');
|
||||
header.appendChild(iconEl);
|
||||
|
||||
// Create title text
|
||||
const titleEl = document.createElement('div');
|
||||
titleEl.style.cssText = 'font-size: 18px; font-weight: 600; color: var(--text-normal);';
|
||||
titleEl.textContent = 'Performance Warning';
|
||||
header.appendChild(titleEl);
|
||||
modal.appendChild(header);
|
||||
|
||||
// Warning message
|
||||
const message = document.createElement('div');
|
||||
message.style.cssText = 'margin-bottom: 20px; line-height: 1.5; color: var(--text-normal);';
|
||||
|
||||
// Create intro paragraph
|
||||
const intro = document.createElement('p');
|
||||
intro.style.cssText = 'margin: 0 0 12px 0;';
|
||||
intro.textContent = 'This query returned ';
|
||||
|
||||
const taskCountStrong = document.createElement('strong');
|
||||
taskCountStrong.textContent = taskCount.toLocaleString() + ' tasks';
|
||||
intro.appendChild(taskCountStrong);
|
||||
|
||||
const introEnd = document.createTextNode(', which may cause performance issues:');
|
||||
intro.appendChild(introEnd);
|
||||
message.appendChild(intro);
|
||||
|
||||
// Create issues list
|
||||
const issuesList = document.createElement('ul');
|
||||
issuesList.style.cssText = 'margin: 0 0 12px 20px; padding: 0;';
|
||||
|
||||
const issues = ['Slow rendering and UI freezing', 'Decreased responsiveness'];
|
||||
issues.forEach(issue => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = issue;
|
||||
issuesList.appendChild(li);
|
||||
});
|
||||
message.appendChild(issuesList);
|
||||
|
||||
// Create advice paragraph
|
||||
const advice = document.createElement('p');
|
||||
advice.style.cssText = 'margin: 0; font-weight: 500;';
|
||||
advice.textContent = 'Consider refining your filter query to show fewer results, or continue if you need to see all tasks.';
|
||||
message.appendChild(advice);
|
||||
|
||||
modal.appendChild(message);
|
||||
|
||||
// Buttons
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.cssText = 'display: flex; gap: 12px; justify-content: flex-end;';
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.style.cssText = `
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-secondary);
|
||||
color: var(--text-normal);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const continueButton = document.createElement('button');
|
||||
continueButton.textContent = `Load All ${taskCount.toLocaleString()} Tasks`;
|
||||
continueButton.style.cssText = `
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
// Button hover effects
|
||||
cancelButton.addEventListener('mouseenter', () => {
|
||||
cancelButton.style.background = 'var(--background-modifier-hover)';
|
||||
});
|
||||
cancelButton.addEventListener('mouseleave', () => {
|
||||
cancelButton.style.background = 'var(--background-secondary)';
|
||||
});
|
||||
|
||||
continueButton.addEventListener('mouseenter', () => {
|
||||
continueButton.style.opacity = '0.9';
|
||||
});
|
||||
continueButton.addEventListener('mouseleave', () => {
|
||||
continueButton.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Event handlers
|
||||
const cleanup = () => {
|
||||
overlay.remove();
|
||||
};
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
continueButton.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
cleanup();
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
buttonContainer.appendChild(cancelButton);
|
||||
buttonContainer.appendChild(continueButton);
|
||||
modal.appendChild(buttonContainer);
|
||||
overlay.appendChild(modal);
|
||||
|
||||
// Add to document
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Focus the continue button by default
|
||||
setTimeout(() => continueButton.focus(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
export function renderBasesDataItem(container: HTMLElement, item: BasesDataItem, index: number): void {
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.className = 'tn-bases-data-item';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import TaskNotesPlugin from '../main';
|
||||
import { BasesDataItem, identifyTaskNotesFromBasesData } from './helpers';
|
||||
import { BasesDataItem, identifyTaskNotesFromBasesData, showPerformanceWarning } from './helpers';
|
||||
import { TaskInfo } from '../types';
|
||||
import { getBasesGroupByConfig, BasesGroupByConfig } from './group-by';
|
||||
import { getGroupNameComparator } from './group-ordering';
|
||||
import { getBasesSortComparator } from './sorting';
|
||||
import { createTaskCard, updateTaskCard } from '../ui/TaskCard';
|
||||
import { setIcon } from 'obsidian';
|
||||
// Removed unused imports - using local BasesContainerLike interface for compatibility
|
||||
|
||||
|
||||
// Use the same interface as base-view-factory for compatibility
|
||||
interface BasesContainerLike {
|
||||
results?: Map<any, any>;
|
||||
|
|
@ -181,6 +183,47 @@ export function buildTasknotesKanbanViewFactory(plugin: TaskNotesPlugin) {
|
|||
const dataItems = extractDataItems();
|
||||
const taskNotes = await identifyTaskNotesFromBasesData(dataItems, plugin);
|
||||
|
||||
// Performance warning for large datasets
|
||||
if (taskNotes.length > 1000) {
|
||||
const shouldContinue = await showPerformanceWarning(taskNotes.length);
|
||||
if (!shouldContinue) {
|
||||
// Show partial message
|
||||
board.innerHTML = '';
|
||||
const partialEl = document.createElement('div');
|
||||
partialEl.className = 'tn-bases-partial';
|
||||
partialEl.style.cssText = 'padding: 20px; text-align: center; color: var(--text-muted);';
|
||||
|
||||
// Create icon and title
|
||||
const titleDiv = document.createElement('div');
|
||||
titleDiv.style.cssText = 'margin-bottom: 8px; display: flex; align-items: center; justify-content: center; gap: 8px;';
|
||||
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.style.cssText = 'color: var(--text-warning); display: flex; align-items: center;';
|
||||
setIcon(iconEl, 'alert-triangle');
|
||||
titleDiv.appendChild(iconEl);
|
||||
|
||||
const titleText = document.createElement('span');
|
||||
titleText.textContent = 'Performance limit reached';
|
||||
titleDiv.appendChild(titleText);
|
||||
partialEl.appendChild(titleDiv);
|
||||
|
||||
// Create description
|
||||
const description = document.createElement('div');
|
||||
description.style.cssText = 'font-size: 0.9em;';
|
||||
description.textContent = `Found ${taskNotes.length} tasks, but loading was cancelled to prevent performance issues.`;
|
||||
partialEl.appendChild(description);
|
||||
|
||||
// Create advice
|
||||
const advice = document.createElement('div');
|
||||
advice.style.cssText = 'font-size: 0.9em; margin-top: 8px;';
|
||||
advice.textContent = 'Consider refining your query to show fewer results.';
|
||||
partialEl.appendChild(advice);
|
||||
|
||||
board.appendChild(partialEl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (taskNotes.length === 0) {
|
||||
board.innerHTML = '';
|
||||
const empty = document.createElement('div');
|
||||
|
|
|
|||
Loading…
Reference in a new issue