[Bug]: Default tasks folder and Archive folder fields too short Generated by ai-issue-analyzer
8.4 KiB
Issue #789: Folder Path Truncation Bug
Problem Understanding
Issue: The path fields for "Default tasks folder" and "Archive folder" allow entering long paths, but sometimes lose characters at the end of the path when saving.
Example:
- Input:
0 System/004 Plugin Data/TaskNotes/Archive - Saved:
0 System/004 Plugin Data/TaskNotes/Archi
Reported Behavior:
- The user reports it doesn't seem to be strictly about length (not a 40-char limit)
- Sometimes chars are lost at the end of the path
- This causes tasks to be created in wrong paths
Test File Location
File: /home/calluma/projects/tasknotes/tests/unit/issues/issue-789-folder-path-truncation.test.ts
How to Run:
npm test -- issue-789-folder-path-truncation.test.ts
The test demonstrates:
- How debounce with
immediate=trueblocks rapid subsequent calls - How this can cause data loss when multiple onChange events occur in quick succession
- How a trailing debounce (immediate=false) would prevent this issue
Root Cause Analysis
The bug is caused by the debounce implementation with immediate=true in the settings tab:
Relevant Code Locations
-
TaskNotesSettingTab.ts:22
private debouncedSave = debounce(() => this.plugin.saveSettings(), 500, true);- Uses debounce with
immediate=true - This causes the save to execute immediately on the first onChange
- Then blocks subsequent saves for 500ms
- Uses debounce with
-
settingHelpers.ts:67-94 -
createTextSettingfunction- Lines 74-80: Handles debounced vs immediate onChange
- The text settings pass the user's setValue function directly to onChange
- When debounceMs is NOT set, onChange fires immediately for each character typed
-
generalTab.ts:29-39 - Default tasks folder setting
createTextSetting(container, { name: translate("settings.general.taskStorage.defaultFolder.name"), getValue: () => plugin.settings.tasksFolder, setValue: async (value: string) => { plugin.settings.tasksFolder = value; save(); // <- calls debouncedSave }, }); -
generalTab.ts:54-64 - Archive folder setting
createTextSetting(container, { name: translate("settings.general.taskStorage.archiveFolder.name"), getValue: () => plugin.settings.archiveFolder, setValue: async (value: string) => { plugin.settings.archiveFolder = value; save(); // <- calls debouncedSave }, }); -
main.ts:1200-1208 - saveSettings function
async saveSettings() { const data = (await this.loadData()) || {}; const settingsKeys = Object.keys(DEFAULT_SETTINGS) as (keyof TaskNotesSettings)[]; for (const key of settingsKeys) { data[key] = this.settings[key]; } await this.saveData(data); }
How the Bug Occurs
Scenario 1: Quick Typing
- User types quickly: "Archive" → each character triggers onChange
- First onChange saves immediately (due to immediate=true)
- Subsequent onChange events are blocked for 500ms
- Only partial path gets saved
Scenario 2: Paste + Autocomplete
- User pastes or uses autocomplete: "Archi" → onChange fired
- Save happens immediately with incomplete path
- Autocomplete finishes: "Archive" → onChange fired
- Blocked by debounce cooldown, never saved
Scenario 3: Browser Autofill
- Browser autofills field with partial text
- onChange fires, saves immediately
- Browser finishes autofilling
- Final onChange is blocked
Proposed Solutions
Solution 1: Use Trailing Debounce (Recommended)
Change: Switch from immediate=true to immediate=false in the debounce call.
Implementation:
// In src/settings/TaskNotesSettingTab.ts:22
private debouncedSave = debounce(() => this.plugin.saveSettings(), 500, false);
// Or simply omit the third parameter (defaults to false)
private debouncedSave = debounce(() => this.plugin.saveSettings(), 500);
Pros:
- Simple one-line fix
- Ensures the final value is always saved
- Reduces number of save operations (better for performance)
- Consistent with common debounce patterns (e.g., search inputs)
Cons:
- Introduces a 500ms delay before settings are saved
- User might close settings before the save completes
- Changes are not persisted until user stops typing
Risk Level: Low - Well-tested pattern
Solution 2: Add Per-Field Debounce for Text Inputs
Change: Add debounce directly to text inputs instead of the save function.
Implementation:
// In src/settings/tabs/generalTab.ts
createTextSetting(container, {
name: translate("settings.general.taskStorage.defaultFolder.name"),
getValue: () => plugin.settings.tasksFolder,
setValue: async (value: string) => {
plugin.settings.tasksFolder = value;
save();
},
debounceMs: 500, // Add this to problematic fields
});
Pros:
- More granular control over which fields are debounced
- Save function can remain immediate for other settings
- Can tune debounce time per field type
Cons:
- Requires updating multiple field definitions
- Dual debouncing (field + save) might cause confusion
- More code changes required
Risk Level: Medium - Requires changes in multiple files
Solution 3: Hybrid Approach - Trailing Debounce with Blur Event Save
Change: Use trailing debounce + save on blur/unfocus events.
Implementation:
// In src/settings/TaskNotesSettingTab.ts
private debouncedSave = debounce(() => this.plugin.saveSettings(), 500, false);
// In settingHelpers.ts - enhance createTextSetting
export function createTextSetting(container: HTMLElement, options: TextSettingOptions): Setting {
return new Setting(container)
.setName(options.name)
.setDesc(options.desc)
.addText((text) => {
text.setValue(options.getValue());
if (options.debounceMs && options.debounceMs > 0) {
const debouncedSetValue = debounce(options.setValue, options.debounceMs);
text.onChange(debouncedSetValue);
} else {
text.onChange(options.setValue);
}
// Add blur event to ensure save on focus loss
text.inputEl.addEventListener('blur', () => {
options.setValue(text.getValue());
});
// ... rest of the function
});
}
Pros:
- Best of both worlds: debounced during typing, immediate on blur
- Guarantees settings are saved when user navigates away
- No data loss risk
- Responsive user experience
Cons:
- Most complex solution
- Might save twice in some cases (onChange + blur)
- Requires modification to helper function
Risk Level: Medium - More complex but safer
Recommended Approach
Solution 1: Use Trailing Debounce
This is the simplest and most reliable fix. The 500ms delay is acceptable for settings that don't need instant persistence. Most users expect some delay in settings panels.
Why this solution:
- Minimal code change - One line modification
- Proven pattern - Standard approach used in many applications
- Fixes the root cause - Ensures final value is always saved
- Better performance - Reduces unnecessary save operations
- Low risk - Well-understood behavior
Additional Improvement: Consider reducing the debounce time from 500ms to 300ms for a more responsive feel while still preventing excessive saves.
Implementation Plan:
- Change
TaskNotesSettingTab.ts:22fromimmediate=truetoimmediate=false(or omit parameter) - Test with long folder paths (paste, type quickly, autocomplete scenarios)
- Verify settings persist correctly when closing the settings panel
- Consider adding a blur event listener as a safety net (optional)
Testing Strategy
-
Manual Testing:
- Paste long folder path → verify full path is saved
- Type quickly → verify complete path is saved
- Use autocomplete → verify completed path is saved
- Close settings immediately after typing → verify save completes
-
Automated Testing:
- Run existing test:
npm test -- issue-789-folder-path-truncation.test.ts - Add integration test for settings save behavior
- Test debounce timing edge cases
- Run existing test:
-
Regression Testing:
- Verify other text settings still work correctly
- Test all tabs in settings panel
- Verify performance hasn't degraded