2025-03-16 19:20:48 +00:00
import {
App ,
Plugin ,
PluginSettingTab ,
Setting ,
WorkspaceLeaf ,
TFile ,
MarkdownView ,
ItemView ,
Editor ,
MarkdownPostProcessorContext ,
debounce ,
Notice ,
2025-07-12 09:25:57 +00:00
SuggestModal ,
2025-03-16 19:20:48 +00:00
} from "obsidian" ;
2022-04-15 18:13:31 +00:00
2025-07-12 09:25:57 +00:00
/ * *
* Debug logger utility that only logs when debug mode is enabled
* Prevents console spam in production builds
* /
class DebugLogger {
private isDebugEnabled : ( ) = > boolean ;
constructor ( isDebugEnabled : ( ) = > boolean ) {
this . isDebugEnabled = isDebugEnabled ;
}
log ( message : string , . . . args : any [ ] ) : void {
if ( this . isDebugEnabled ( ) ) {
console . log ( ` [Progress Tracker] ${ message } ` , . . . args ) ;
}
}
error ( message : string , error? : Error ) : void {
if ( this . isDebugEnabled ( ) ) {
console . error ( ` [Progress Tracker ERROR] ${ message } ` , error ) ;
}
}
warn ( message : string , . . . args : any [ ] ) : void {
if ( this . isDebugEnabled ( ) ) {
console . warn ( ` [Progress Tracker WARNING] ${ message } ` , . . . args ) ;
}
}
}
/ * *
* Type - safe interface for Dataview API
* /
2025-03-16 09:26:27 +00:00
interface DataviewApi {
2025-03-16 19:20:48 +00:00
executeJs (
code : string ,
container : HTMLElement ,
sourcePath? : string
) : Promise < any > ;
2025-03-16 09:26:27 +00:00
page ( path : string ) : any ;
pages ( source : string ) : any [ ] ;
}
2025-07-12 09:25:57 +00:00
/ * *
* Type - safe interface for accessing plugins
* /
interface ObsidianApp extends App {
plugins ? : {
plugins ? : {
dataview ? : {
api? : DataviewApi ;
} ;
} ;
enabledPlugins? : Set < string > ;
} ;
}
/ * *
* Extended window interface for Dataview API access
* /
2025-03-16 09:26:27 +00:00
declare global {
interface Window {
DataviewAPI? : DataviewApi ;
}
}
2025-07-12 09:25:57 +00:00
/ * *
* Safely get Dataview API with proper type checking
* @param app - Obsidian app instance
* @returns DataviewApi instance or null if not available
* /
2025-03-16 09:26:27 +00:00
function getDataviewAPI ( app : App ) : DataviewApi | null {
2025-07-12 09:25:57 +00:00
try {
// Method 1: Through window object (most reliable)
2025-12-04 09:22:45 +00:00
if ( typeof window !== "undefined" && window . DataviewAPI ) {
2025-07-12 09:25:57 +00:00
return window . DataviewAPI ;
}
2025-03-16 19:20:48 +00:00
2025-07-12 09:25:57 +00:00
// Method 2: Through app.plugins with type safety
const obsidianApp = app as ObsidianApp ;
const dataviewPlugin = obsidianApp . plugins ? . plugins ? . dataview ;
if ( dataviewPlugin ? . api ) {
return dataviewPlugin . api ;
}
// Method 3: Check if plugin is enabled
const enabledPlugins = obsidianApp . plugins ? . enabledPlugins ;
if ( enabledPlugins ? . has ( "dataview" ) ) {
// Plugin is enabled but API not ready yet
return null ;
}
2025-03-16 19:20:48 +00:00
2025-07-12 09:25:57 +00:00
return null ;
} catch ( error ) {
console . error ( "Error accessing Dataview API:" , error ) ;
2025-03-16 09:26:27 +00:00
return null ;
}
}
2022-04-15 18:13:31 +00:00
2025-07-12 08:57:43 +00:00
// Define custom checkbox mapping interface
interface KanbanColumnCheckboxMapping {
columnName : string ;
checkboxState : string ; // e.g., "[ ]", "[/]", "[x]", "[>]", etc.
}
2025-03-16 09:26:27 +00:00
interface TaskProgressBarSettings {
showDebugInfo : boolean ;
2025-03-16 19:20:48 +00:00
progressColorScheme : "default" | "red-orange-green" | "custom" ;
2025-03-16 11:34:29 +00:00
lowProgressColor : string ;
mediumProgressColor : string ;
highProgressColor : string ;
completeProgressColor : string ;
lowProgressThreshold : number ;
mediumProgressThreshold : number ;
highProgressThreshold : number ;
2025-03-20 09:43:08 +00:00
showUpdateAnimation : boolean ;
updateAnimationDelay : number ;
editorChangeDelay : number ;
keyboardInputDelay : number ;
checkboxClickDelay : number ;
maxTabsHeight : string ;
autoUpdateMetadata : boolean ;
autoChangeStatus : boolean ;
autoUpdateFinishedDate : boolean ;
autoUpdateKanban : boolean ;
kanbanCompletedColumn : string ; // Deprecated but kept for backward compatibility
statusTodo : string ;
statusInProgress : string ;
statusCompleted : string ;
kanbanAutoDetect : boolean ;
kanbanSpecificFiles : string [ ] ;
kanbanExcludeFiles : string [ ] ;
kanbanSyncWithStatus : boolean ;
2025-03-24 09:40:04 +00:00
autoAddToKanban : boolean ;
autoAddKanbanBoard : string ;
autoAddKanbanColumn : string ;
2025-07-12 08:57:43 +00:00
// New settings for custom checkbox states
enableCustomCheckboxStates : boolean ;
kanbanColumnCheckboxMappings : KanbanColumnCheckboxMapping [ ] ;
// New setting for reverse Kanban sync
enableKanbanToFileSync : boolean ;
// New setting for auto-sync checkbox states on Kanban open
enableKanbanAutoSync : boolean ;
// NEW: Protection from Kanban normalization
enableKanbanNormalizationProtection : boolean ;
2022-04-15 18:13:31 +00:00
}
2025-03-16 09:26:27 +00:00
const DEFAULT_SETTINGS : TaskProgressBarSettings = {
2025-03-16 11:34:29 +00:00
showDebugInfo : false ,
2025-03-16 19:20:48 +00:00
progressColorScheme : "default" ,
lowProgressColor : "#e06c75" , // Red
mediumProgressColor : "#e5c07b" , // Orange/Yellow
highProgressColor : "#61afef" , // Blue
completeProgressColor : "#98c379" , // Green
2025-03-16 11:34:29 +00:00
lowProgressThreshold : 33 ,
mediumProgressThreshold : 66 ,
2025-03-16 18:17:32 +00:00
highProgressThreshold : 99 ,
2025-03-20 09:43:08 +00:00
showUpdateAnimation : true ,
2025-05-16 05:31:46 +00:00
updateAnimationDelay : 150 , // Reduced from 300ms to 150ms
editorChangeDelay : 200 , // Reduced from 500ms to 200ms
keyboardInputDelay : 50 , // Reduced from 100ms to 50ms
checkboxClickDelay : 100 , // Reduced from 200ms to 100ms
2025-03-20 09:43:08 +00:00
maxTabsHeight : "auto" ,
autoUpdateMetadata : true ,
autoChangeStatus : true ,
autoUpdateFinishedDate : true ,
autoUpdateKanban : true ,
kanbanCompletedColumn : "Complete" , // Deprecated
statusTodo : "Todo" ,
statusInProgress : "In Progress" ,
statusCompleted : "Completed" ,
kanbanAutoDetect : true ,
kanbanSpecificFiles : [ ] ,
kanbanExcludeFiles : [ ] ,
kanbanSyncWithStatus : true ,
2025-03-24 09:40:04 +00:00
autoAddToKanban : false ,
autoAddKanbanBoard : "" ,
autoAddKanbanColumn : "Todo" ,
2025-07-12 08:57:43 +00:00
enableCustomCheckboxStates : false ,
kanbanColumnCheckboxMappings : [
{ columnName : "Todo" , checkboxState : "[ ]" } ,
{ columnName : "In Progress" , checkboxState : "[/]" } ,
{ columnName : "Complete" , checkboxState : "[x]" } ,
{ columnName : "Done" , checkboxState : "[x]" } ,
] ,
enableKanbanToFileSync : false ,
enableKanbanAutoSync : false ,
enableKanbanNormalizationProtection : true ,
2025-03-16 19:20:48 +00:00
} ;
2022-04-15 18:13:31 +00:00
2025-03-16 09:26:27 +00:00
export default class TaskProgressBarPlugin extends Plugin {
settings : TaskProgressBarSettings ;
dvAPI : DataviewApi | null = null ;
2025-03-16 11:34:29 +00:00
sidebarView : TaskProgressBarView | null = null ;
2025-03-16 09:26:27 +00:00
private lastActiveFile : TFile | null = null ;
2025-03-16 19:20:48 +00:00
private lastFileContent : string = "" ;
2025-03-16 09:26:27 +00:00
private dataviewCheckInterval : number | null = null ;
2025-07-12 09:25:57 +00:00
// Debug logger instance
private logger : DebugLogger ;
2025-07-12 08:57:43 +00:00
// New tracking variables for Kanban-to-file sync
private lastKanbanContent : Map < string , string > = new Map ( ) ;
private isUpdatingFromKanban : boolean = false ;
// Track which Kanban files have been auto-synced to avoid repeat runs
private autoSyncedFiles : Set < string > = new Set ( ) ;
// Track last update time for each file to detect timing conflicts
private lastFileUpdateMap : Map < string , number > = new Map ( ) ;
// NEW: Smart Kanban interaction detection
2025-12-04 09:22:45 +00:00
private kanbanNormalizationDetector : Map <
string ,
{
preChangeCheckpoints : Map < number , string > ;
lastKanbanUIInteraction : number ;
pendingNormalizationCheck : number | null ;
}
> = new Map ( ) ;
2022-04-15 18:13:31 +00:00
async onload() {
await this . loadSettings ( ) ;
2025-07-12 09:25:57 +00:00
// Initialize debug logger
this . logger = new DebugLogger ( ( ) = > this . settings . showDebugInfo ) ;
2025-03-19 11:33:52 +00:00
// Apply the max-height CSS style as soon as the plugin loads
2025-03-16 19:20:48 +00:00
this . applyMaxTabsHeightStyle ( ) ;
2025-03-16 09:26:27 +00:00
// Register view type for the sidebar
this . registerView (
2025-03-16 19:20:48 +00:00
"progress-tracker" ,
2025-03-16 09:26:27 +00:00
( leaf ) = > ( this . sidebarView = new TaskProgressBarView ( leaf , this ) )
) ;
// Add icon to the left sidebar
2025-03-16 19:20:48 +00:00
this . addRibbonIcon ( "bar-chart-horizontal" , "Progress Tracker" , ( ) = > {
2025-03-16 09:26:27 +00:00
this . activateView ( ) ;
2022-04-15 18:13:31 +00:00
} ) ;
2025-03-16 09:26:27 +00:00
// Add settings tab
this . addSettingTab ( new TaskProgressBarSettingTab ( this . app , this ) ) ;
2025-04-15 11:25:00 +00:00
// Check Dataview API and set up interval to check again if not found
2025-03-16 09:26:27 +00:00
this . checkDataviewAPI ( ) ;
2025-03-16 19:20:48 +00:00
2025-03-16 09:26:27 +00:00
// Register event to update progress bar when file changes
this . registerEvent (
2025-03-16 19:20:48 +00:00
this . app . workspace . on ( "file-open" , ( file ) = > {
2025-03-16 09:26:27 +00:00
if ( file ) {
this . lastActiveFile = file ;
2025-03-16 19:20:48 +00:00
2025-07-12 08:57:43 +00:00
// Handle auto-sync for Kanban boards (run once per file per session)
2025-12-04 09:22:45 +00:00
if (
this . settings . enableKanbanAutoSync &&
2025-07-12 08:57:43 +00:00
this . settings . enableCustomCheckboxStates &&
this . isKanbanBoard ( file ) &&
! this . autoSyncedFiles . has ( file . path ) &&
2025-12-04 09:22:45 +00:00
! this . isUpdatingFromKanban
) {
this . logger . log (
` Auto-syncing Kanban board on open: ${ file . path } `
) ;
2025-07-12 08:57:43 +00:00
// Add small delay to ensure file is fully loaded and no other operations are running
setTimeout ( async ( ) = > {
// Triple-check that we're not in the middle of an update and no recent changes
2025-12-04 09:22:45 +00:00
if (
! this . isUpdatingFromKanban &&
! this . lastKanbanContent . has ( file . path )
) {
2025-07-12 08:57:43 +00:00
await this . autoSyncKanbanCheckboxStates ( file ) ;
2025-07-12 09:25:57 +00:00
} else {
2025-12-04 09:22:45 +00:00
this . logger . log (
` Skipping auto-sync - update in progress or file already tracked `
) ;
2025-07-12 08:57:43 +00:00
}
} , 800 ) ; // Increased delay to avoid conflicts with editor changes
}
// Original progress bar update logic
2025-03-17 10:53:48 +00:00
setTimeout ( async ( ) = > {
await this . updateLastFileContent ( file ) ;
if ( this . sidebarView ) {
// Pass true to force update even for files without tasks
this . sidebarView . updateProgressBar ( file ) ;
}
2025-05-16 05:01:17 +00:00
} , 100 ) ;
2025-03-16 09:26:27 +00:00
}
} )
) ;
2022-04-15 18:13:31 +00:00
2025-07-12 08:57:43 +00:00
// Register event to listen for file modifications (for Kanban UI changes)
this . registerEvent (
this . app . vault . on ( "modify" , async ( file ) = > {
// Only handle TFile instances that are Kanban boards
2025-12-04 09:22:45 +00:00
if (
file instanceof TFile &&
this . settings . enableKanbanToFileSync &&
2025-07-12 08:57:43 +00:00
this . settings . enableCustomCheckboxStates &&
2025-12-04 09:22:45 +00:00
this . isKanbanBoard ( file )
) {
this . logger . log (
` File modified event for Kanban board: ${ file . path } `
) ;
2025-07-12 08:57:43 +00:00
// Skip if we're currently updating to avoid loops
if ( this . isUpdatingFromKanban ) {
2025-12-04 09:22:45 +00:00
this . logger . log (
"Skipping file modify - currently updating from plugin"
) ;
2025-07-12 08:57:43 +00:00
return ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Add small delay to ensure content is properly updated
setTimeout ( async ( ) = > {
try {
const newContent = await this . app . vault . read ( file ) ;
2025-12-04 09:22:45 +00:00
await this . handleKanbanBoardChange (
file ,
newContent
) ;
2025-07-12 08:57:43 +00:00
} catch ( error ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . error (
"Error handling file modify for Kanban board:" ,
error
) ;
2025-07-12 08:57:43 +00:00
}
}
} , 100 ) ;
}
} )
) ;
2025-03-16 09:26:27 +00:00
// Register event to update progress bar when editor changes
this . registerEvent (
2025-03-16 19:20:48 +00:00
this . app . workspace . on (
"editor-change" ,
debounce ( async ( editor , view ) = > {
if ( view instanceof MarkdownView && this . sidebarView ) {
2025-07-12 08:57:43 +00:00
// Skip if we're currently updating from Kanban to avoid loops
if ( this . isUpdatingFromKanban ) {
2025-12-04 09:22:45 +00:00
this . logger . log (
"Skipping editor-change - currently updating from Kanban"
) ;
2025-07-12 08:57:43 +00:00
return ;
}
2025-03-20 11:32:16 +00:00
// Get current editor content
2025-03-16 19:20:48 +00:00
const content = editor . getValue ( ) ;
2025-07-12 08:57:43 +00:00
const currentFile = view . file ;
// Check if this is a Kanban board file and handle card checkbox sync (independent feature)
2025-12-04 09:22:45 +00:00
if (
this . settings . enableKanbanToFileSync &&
2025-07-12 08:57:43 +00:00
this . settings . enableCustomCheckboxStates &&
2025-12-04 09:22:45 +00:00
currentFile &&
this . isKanbanBoard ( currentFile )
) {
this . logger . log (
` Detected Kanban board change: ${ currentFile . path } `
) ;
this . logger . log (
` Content length: ${ content . length } , lastFileContent length: ${ this . lastFileContent . length } `
) ;
this . logger . log (
` isUpdatingFromKanban: ${ this . isUpdatingFromKanban } `
) ;
2025-07-12 08:57:43 +00:00
// NEW: Enhanced immediate Kanban normalization protection (runs for ALL Kanban changes)
2025-12-04 09:22:45 +00:00
if (
this . settings
. enableKanbanNormalizationProtection
) {
const hasImmediateNormalization =
this . detectImmediateKanbanNormalization (
this . lastFileContent ,
content
) ;
this . logger . log (
` Immediate normalization check: ${ hasImmediateNormalization } `
) ;
2025-07-12 08:57:43 +00:00
if ( hasImmediateNormalization ) {
2025-12-04 09:22:45 +00:00
this . logger . log (
"Detected immediate Kanban normalization - reverting unwanted changes"
) ;
2025-07-12 08:57:43 +00:00
// Revert the normalization immediately
2025-12-04 09:22:45 +00:00
const revertedContent =
this . revertKanbanNormalization (
this . lastFileContent ,
content ,
currentFile
) ;
2025-07-12 08:57:43 +00:00
if ( revertedContent !== content ) {
// Set flag to prevent loops
this . isUpdatingFromKanban = true ;
// Apply the reverted content and then sync all checkbox states
setTimeout ( async ( ) = > {
// Apply sync to ensure all checkbox states match column mappings
2025-12-04 09:22:45 +00:00
const syncedContent =
await this . syncAllCheckboxStatesToMappings (
currentFile ,
revertedContent
) ;
await this . app . vault . modify (
currentFile ,
syncedContent
) ;
this . lastFileContent =
syncedContent ;
2025-07-12 08:57:43 +00:00
// Try to force refresh Kanban UI
2025-12-04 09:22:45 +00:00
await this . forceRefreshKanbanUI (
currentFile
) ;
2025-07-12 08:57:43 +00:00
// Reset flag
setTimeout ( ( ) = > {
2025-12-04 09:22:45 +00:00
this . isUpdatingFromKanban =
false ;
2025-07-12 08:57:43 +00:00
} , 100 ) ;
} , 50 ) ;
return ; // Skip further processing
}
} else {
// DEBUG: If no immediate normalization detected, let's see what changed
2025-12-04 09:22:45 +00:00
const oldLines =
this . lastFileContent . split ( "\n" ) ;
const newLines = content . split ( "\n" ) ;
2025-07-12 09:25:57 +00:00
let changeCount = 0 ;
2025-12-04 09:22:45 +00:00
for (
let i = 0 ;
i <
Math . min (
oldLines . length ,
newLines . length
) ;
i ++
) {
const oldMatch =
oldLines [ i ] . match (
/^(\s*- )\[([^\]]*)\]/
) ;
const newMatch =
newLines [ i ] . match (
/^(\s*- )\[([^\]]*)\]/
) ;
if (
oldMatch &&
newMatch &&
oldMatch [ 2 ] !== newMatch [ 2 ]
) {
2025-07-12 09:25:57 +00:00
changeCount ++ ;
2025-12-04 09:22:45 +00:00
this . logger . log (
` Line ${ i } checkbox change: [ ${ oldMatch [ 2 ] } ] → [ ${ newMatch [ 2 ] } ] `
) ;
2025-07-12 08:57:43 +00:00
}
}
2025-07-12 09:25:57 +00:00
if ( changeCount > 0 ) {
2025-12-04 09:22:45 +00:00
this . logger . log (
` Found ${ changeCount } checkbox changes but no immediate normalization detected `
) ;
2025-07-12 09:25:57 +00:00
}
2025-07-12 08:57:43 +00:00
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Add small delay to ensure content is properly updated after drag/drop
setTimeout ( async ( ) = > {
// Re-read the file content to ensure we have the latest version
2025-12-04 09:22:45 +00:00
const latestContent = await this . app . vault . read (
currentFile
) ;
await this . handleKanbanBoardChange (
currentFile ,
latestContent
) ;
2025-07-12 08:57:43 +00:00
} , 200 ) ;
// Don't return here - allow normal processing to continue
}
2025-03-16 19:20:48 +00:00
2025-07-12 08:57:43 +00:00
// Original logic for regular file changes (including file-to-kanban sync)
// Check if content contains tasks (enhanced to support custom checkbox states)
2025-03-16 19:20:48 +00:00
if (
content . includes ( "- [" ) ||
this . lastFileContent . includes ( "- [" ) ||
2025-07-12 08:57:43 +00:00
/- \[[^\]]*\]/ . test ( content ) ||
/- \[[^\]]*\]/ . test ( this . lastFileContent )
2025-03-16 19:20:48 +00:00
) {
2025-05-16 05:31:46 +00:00
// Check if tasks have changed
2025-12-04 09:22:45 +00:00
if (
this . hasTaskContentChanged (
this . lastFileContent ,
content
)
) {
2025-05-16 05:21:07 +00:00
if ( this . lastActiveFile ) {
// Update last file content before checking changes
this . lastActiveFile = view . file ;
// Update progress bar immediately
this . sidebarView . updateProgressBar (
view . file ,
content
) ;
// Then update last file content
this . lastFileContent = content ;
}
2025-07-12 09:25:57 +00:00
} else {
2025-12-04 09:22:45 +00:00
this . logger . log (
"Skipping update - no task changes detected"
) ;
2025-03-16 19:20:48 +00:00
}
2025-03-16 09:26:27 +00:00
}
}
2025-03-16 19:20:48 +00:00
} , this . settings . editorChangeDelay )
2025-05-16 05:21:07 +00:00
)
2025-03-16 09:26:27 +00:00
) ;
2025-03-20 11:32:16 +00:00
// Listen for keydown events to detect when user enters new tasks or checks/unchecks tasks
2025-03-16 19:20:48 +00:00
this . registerDomEvent ( document , "keydown" , ( evt : KeyboardEvent ) = > {
2025-03-20 11:32:16 +00:00
// Check if we're in the editor
2025-03-16 19:20:48 +00:00
const activeView =
this . app . workspace . getActiveViewOfType ( MarkdownView ) ;
if ( activeView && activeView . getMode ( ) === "source" ) {
2025-03-20 11:32:16 +00:00
// Update immediately when pressing keys related to tasks
2025-03-16 19:20:48 +00:00
if (
[
"Enter" ,
"Space" ,
"]" ,
"x" ,
"X" ,
"Backspace" ,
"Delete" ,
] . includes ( evt . key )
) {
2025-03-20 11:32:16 +00:00
// Update immediately
2025-03-16 09:26:27 +00:00
setTimeout ( ( ) = > {
const content = activeView . editor . getValue ( ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check if content contains tasks and if they have changed (enhanced for custom states)
2025-03-16 19:20:48 +00:00
if (
2025-05-16 05:21:07 +00:00
( content . includes ( "- [" ) ||
2025-12-04 09:22:45 +00:00
/- \[[^\]]*\]/ . test ( content ) ) &&
this . hasTaskContentChanged (
this . lastFileContent ,
content
)
2025-03-16 19:20:48 +00:00
) {
2025-03-16 09:26:27 +00:00
this . lastActiveFile = activeView . file ;
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// Update progress bar immediately
2025-03-16 09:26:27 +00:00
if ( this . sidebarView ) {
2025-03-16 19:20:48 +00:00
this . sidebarView . updateProgressBar (
activeView . file ,
content
) ;
2025-03-16 09:26:27 +00:00
}
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// Then update last file content
2025-03-16 09:26:27 +00:00
this . lastFileContent = content ;
}
2025-05-16 05:21:07 +00:00
} , this . settings . keyboardInputDelay ) ;
2022-04-15 18:13:31 +00:00
}
}
} ) ;
2025-03-20 11:32:16 +00:00
// Listen for click events in the editor to detect when tasks are checked/unchecked
2025-03-16 19:20:48 +00:00
this . registerDomEvent ( document , "click" , ( evt : MouseEvent ) = > {
2025-03-16 09:26:27 +00:00
const target = evt . target as HTMLElement ;
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// Check if click is on a task checkbox
2025-03-16 19:20:48 +00:00
if (
target &&
target . tagName === "INPUT" &&
target . classList . contains ( "task-list-item-checkbox" )
) {
2025-03-20 11:32:16 +00:00
// Wait a bit for Obsidian to update the task state in the file
2025-03-16 09:26:27 +00:00
setTimeout ( async ( ) = > {
const activeFile = this . app . workspace . getActiveFile ( ) ;
if ( activeFile && this . sidebarView ) {
2025-03-20 11:32:16 +00:00
// Read current file content
2025-03-16 09:26:27 +00:00
const content = await this . app . vault . read ( activeFile ) ;
2025-03-16 19:20:48 +00:00
2025-05-16 05:31:46 +00:00
// Only update if tasks have changed
2025-12-04 09:22:45 +00:00
if (
this . hasTaskContentChanged (
this . lastFileContent ,
content
)
) {
2025-05-16 05:21:07 +00:00
// Update progress bar immediately
this . lastActiveFile = activeFile ;
2025-12-04 09:22:45 +00:00
this . sidebarView . updateProgressBar (
activeFile ,
content
) ;
2025-03-16 19:20:48 +00:00
2025-05-16 05:21:07 +00:00
// Then update last file content
this . lastFileContent = content ;
}
2025-03-16 19:20:48 +00:00
}
2025-05-16 05:21:07 +00:00
} , this . settings . checkboxClickDelay ) ;
2025-03-16 09:26:27 +00:00
}
2022-04-15 18:13:31 +00:00
} ) ;
2025-03-20 11:32:16 +00:00
// Activate view when plugin loads - wait a bit for Obsidian to fully start
2025-03-16 09:26:27 +00:00
setTimeout ( ( ) = > {
this . activateView ( ) ;
2025-03-16 19:20:48 +00:00
// We'll use a single delayed update instead of multiple updates
setTimeout ( async ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . sidebarView ) {
2025-12-04 09:22:45 +00:00
this . logger . log (
"Initial file load after plugin start:" ,
currentFile . path
) ;
2025-03-16 19:20:48 +00:00
await this . updateLastFileContent ( currentFile ) ;
// Use a flag to indicate this is the initial load
this . sidebarView . updateProgressBar (
currentFile ,
undefined ,
true
) ;
}
} , 1500 ) ;
2025-03-16 09:26:27 +00:00
} , 1000 ) ;
2025-03-19 11:33:52 +00:00
// Register commands
this . addCommand ( {
2025-03-20 07:21:00 +00:00
id : "clear-completed-files-cache" ,
2025-04-15 10:53:27 +00:00
name : "Clear completed files cache" ,
2025-03-19 11:33:52 +00:00
callback : ( ) = > {
if ( this . sidebarView ) {
this . sidebarView . clearCompletedFilesCache ( ) ;
2025-03-20 07:21:00 +00:00
new Notice (
"Completed files cache cleared. Files can trigger completion notifications again."
) ;
2025-03-19 11:33:52 +00:00
}
2025-03-20 07:21:00 +00:00
} ,
2025-03-19 11:33:52 +00:00
} ) ;
2025-07-12 08:57:43 +00:00
// Add debug command for Kanban sync
this . addCommand ( {
id : "debug-kanban-sync" ,
name : "Debug Kanban sync status" ,
callback : ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
2025-07-12 09:25:57 +00:00
const debugData : Record < string , any > = {
"Current file" : currentFile . path ,
2025-12-04 09:22:45 +00:00
enableKanbanToFileSync :
this . settings . enableKanbanToFileSync ,
enableCustomCheckboxStates :
this . settings . enableCustomCheckboxStates ,
enableKanbanAutoSync : this.settings.enableKanbanAutoSync ,
enableKanbanNormalizationProtection :
this . settings . enableKanbanNormalizationProtection ,
isKanbanBoard : this.isKanbanBoard ( currentFile ) ,
"Checkbox mappings" :
this . settings . kanbanColumnCheckboxMappings ,
"Last Kanban content stored" : this . lastKanbanContent . has (
currentFile . path
) ,
2025-07-12 09:25:57 +00:00
"Auto-synced files" : Array . from ( this . autoSyncedFiles ) ,
2025-12-04 09:22:45 +00:00
"Current file auto-synced" : this . autoSyncedFiles . has (
currentFile . path
) ,
2025-07-12 09:25:57 +00:00
"isUpdatingFromKanban flag" : this . isUpdatingFromKanban ,
"lastFileContent length" : this . lastFileContent . length ,
2025-12-04 09:22:45 +00:00
"Last file update timestamp" : this . lastFileUpdateMap . get (
currentFile . path
) ,
2025-07-12 09:25:57 +00:00
} ;
2025-07-12 08:57:43 +00:00
// Show current file content preview
if ( this . lastKanbanContent . has ( currentFile . path ) ) {
2025-12-04 09:22:45 +00:00
const storedContent = this . lastKanbanContent . get (
currentFile . path
) ! ;
debugData [
"Stored content preview"
] = ` ${ storedContent . substring ( 0 , 200 ) } ... ` ;
2025-07-12 08:57:43 +00:00
}
2025-07-12 09:25:57 +00:00
this . debugOutput ( "Kanban Sync Debug Info" , debugData ) ;
2025-12-04 09:22:45 +00:00
new Notice (
"Debug info logged to console. Check Developer Tools."
) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to reset auto-sync cache
this . addCommand ( {
id : "reset-kanban-autosync-cache" ,
name : "Reset Kanban auto-sync cache" ,
callback : ( ) = > {
this . autoSyncedFiles . clear ( ) ;
2025-12-04 09:22:45 +00:00
new Notice (
"Auto-sync cache cleared. Kanban boards will be auto-synced again when opened."
) ;
2025-07-12 09:25:57 +00:00
this . logger . log ( "Auto-sync cache cleared" ) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to test checkbox update function
this . addCommand ( {
id : "test-checkbox-update" ,
name : "Test checkbox update function" ,
callback : ( ) = > {
if ( ! this . settings . showDebugInfo ) {
new Notice ( "Enable debug mode first to use this command." ) ;
return ;
}
// Test cases for the fixed function
const testCases = [
{
input : "- [/] Main task\n - [ ] Sub-task 1\n - [x] Sub-task 2" ,
target : "[x]" ,
2025-12-04 09:22:45 +00:00
expected :
"- [x] Main task\n - [ ] Sub-task 1\n - [x] Sub-task 2" ,
2025-07-12 08:57:43 +00:00
} ,
{
input : "- [ ] Simple task" ,
target : "[/]" ,
2025-12-04 09:22:45 +00:00
expected : "- [/] Simple task" ,
} ,
2025-07-12 08:57:43 +00:00
] ;
console . log ( "=== Testing Checkbox Update Function ===" ) ;
testCases . forEach ( ( testCase , index ) = > {
2025-12-04 09:22:45 +00:00
const result = this . updateCheckboxStateInCardText (
testCase . input ,
testCase . target
) ;
2025-07-12 08:57:43 +00:00
const passed = result === testCase . expected ;
2025-12-04 09:22:45 +00:00
console . log (
` Test ${ index + 1 } : ${ passed ? "PASSED" : "FAILED" } `
) ;
2025-07-12 08:57:43 +00:00
console . log ( ` Input: ${ testCase . input } ` ) ;
console . log ( ` Expected: ${ testCase . expected } ` ) ;
console . log ( ` Got: ${ result } ` ) ;
console . log ( "---" ) ;
} ) ;
// Test position finding function
console . log ( "=== Testing Position Finding Function ===" ) ;
const testContent = [
"## Todo" ,
"- [ ] Task 1" ,
" - Sub item" ,
"- [/] Task 2" ,
"" ,
2025-12-04 09:22:45 +00:00
"## In Progress" ,
2025-07-12 08:57:43 +00:00
"- [/] Task 3" ,
2025-12-04 09:22:45 +00:00
"- [x] Task 4" ,
2025-07-12 08:57:43 +00:00
] ;
const positionTests = [
2025-12-04 09:22:45 +00:00
{
card : "- [ ] Task 1\n - Sub item" ,
column : "Todo" ,
expectedPos : 1 ,
} ,
{
card : "- [/] Task 3" ,
column : "In Progress" ,
expectedPos : 6 ,
} ,
{
card : "- [x] Non-existent" ,
column : "Todo" ,
expectedPos : - 1 ,
} ,
2025-07-12 08:57:43 +00:00
] ;
positionTests . forEach ( ( test , index ) = > {
2025-12-04 09:22:45 +00:00
const result = this . findCardPositionInContent (
test . card ,
testContent ,
test . column
) ;
2025-07-12 08:57:43 +00:00
const passed = result === test . expectedPos ;
2025-12-04 09:22:45 +00:00
console . log (
` Position Test ${ index + 1 } : ${
passed ? "PASSED" : "FAILED"
} `
) ;
2025-07-12 08:57:43 +00:00
console . log ( ` Card: ${ test . card } ` ) ;
console . log ( ` Column: ${ test . column } ` ) ;
console . log ( ` Expected Position: ${ test . expectedPos } ` ) ;
console . log ( ` Got Position: ${ result } ` ) ;
console . log ( "---" ) ;
} ) ;
2025-12-04 09:22:45 +00:00
new Notice (
"Checkbox update and position finding tests completed. Check console for results."
) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to reset conflict state
this . addCommand ( {
id : "reset-kanban-conflicts" ,
name : "Reset Kanban conflict state" ,
callback : ( ) = > {
this . isUpdatingFromKanban = false ;
this . lastKanbanContent . clear ( ) ;
this . autoSyncedFiles . clear ( ) ;
this . kanbanNormalizationDetector . clear ( ) ;
2025-12-04 09:22:45 +00:00
new Notice (
"Kanban conflict state reset. All tracking data cleared."
) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
console . log ( "Reset all Kanban conflict tracking:" ) ;
console . log ( "- isUpdatingFromKanban: false" ) ;
console . log ( "- lastKanbanContent: cleared" ) ;
console . log ( "- autoSyncedFiles: cleared" ) ;
console . log ( "- kanbanNormalizationDetector: cleared" ) ;
}
} ,
} ) ;
// Add command to test normalization detection
this . addCommand ( {
id : "test-kanban-normalization-detection" ,
name : "Test Kanban normalization detection" ,
callback : async ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
if ( ! this . isKanbanBoard ( currentFile ) ) {
new Notice ( "Current file is not a Kanban board" ) ;
return ;
}
if ( ! this . settings . showDebugInfo ) {
new Notice ( "Enable debug mode first to use this command." ) ;
return ;
}
const content = await this . app . vault . read ( currentFile ) ;
console . log ( "=== Kanban Normalization Detection Test ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
2025-12-04 09:22:45 +00:00
console . log (
` Protection enabled: ${ this . settings . enableKanbanNormalizationProtection } `
) ;
2025-07-12 08:57:43 +00:00
// Show current detector state
2025-12-04 09:22:45 +00:00
const detector = this . kanbanNormalizationDetector . get (
currentFile . path
) ;
2025-07-12 08:57:43 +00:00
if ( detector ) {
console . log ( "Detector state:" , {
2025-12-04 09:22:45 +00:00
lastKanbanUIInteraction : new Date (
detector . lastKanbanUIInteraction
) . toISOString ( ) ,
2025-07-12 08:57:43 +00:00
checkpointsCount : detector.preChangeCheckpoints.size ,
2025-12-04 09:22:45 +00:00
pendingCheck :
detector . pendingNormalizationCheck !== null ,
2025-07-12 08:57:43 +00:00
} ) ;
} else {
console . log ( "No detector state found for this file" ) ;
}
// Test pattern detection with sample content
2025-12-04 09:22:45 +00:00
const testOldContent = content . replace ( /\[\/\]/g , "[/]" ) ; // Ensure we have custom states
const testNewContent = content . replace ( /\[\/\]/g , "[x]" ) ; // Simulate normalization
const patterns = this . analyzeCheckboxNormalizationPatterns (
testOldContent ,
testNewContent
) ;
2025-07-12 08:57:43 +00:00
console . log ( "Pattern analysis result:" , patterns ) ;
// Test immediate detection
2025-12-04 09:22:45 +00:00
const hasImmediate = this . detectImmediateKanbanNormalization (
testOldContent ,
testNewContent
) ;
2025-07-12 08:57:43 +00:00
console . log ( "Immediate normalization detection:" , hasImmediate ) ;
// Test revert function
if ( hasImmediate ) {
2025-12-04 09:22:45 +00:00
const reverted = this . revertKanbanNormalization (
testOldContent ,
testNewContent ,
currentFile
) ;
2025-07-12 08:57:43 +00:00
console . log ( "Revert test:" , {
originalLength : testOldContent.length ,
normalizedLength : testNewContent.length ,
revertedLength : reverted.length ,
2025-12-04 09:22:45 +00:00
revertedMatchesOriginal : reverted === testOldContent ,
2025-07-12 08:57:43 +00:00
} ) ;
}
2025-12-04 09:22:45 +00:00
new Notice (
"Normalization detection test completed. Check console for results."
) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to test custom checkbox state counting
this . addCommand ( {
id : "test-custom-checkbox-counting" ,
name : "Test custom checkbox state counting" ,
callback : ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
2025-12-04 09:22:45 +00:00
this . app . vault . read ( currentFile ) . then ( ( content ) = > {
2025-07-12 08:57:43 +00:00
console . log ( "=== Custom Checkbox State Test ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
2025-12-04 09:22:45 +00:00
console . log (
` Custom checkbox states enabled: ${ this . settings . enableCustomCheckboxStates } `
) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . enableCustomCheckboxStates ) {
2025-12-04 09:22:45 +00:00
const taskCounts =
this . countTasksByCheckboxState ( content ) ;
console . log (
"Task counts by checkbox state:" ,
taskCounts
) ;
let incompleteTasks = taskCounts [ " " ] || 0 ;
let completedTasks = taskCounts [ "x" ] || 0 ;
2025-07-12 08:57:43 +00:00
let customStateTasks = 0 ;
2025-12-04 09:22:45 +00:00
for ( const [ state , count ] of Object . entries (
taskCounts
) ) {
if (
state !== " " &&
state !== "x" &&
state . trim ( ) !== ""
) {
2025-07-12 08:57:43 +00:00
customStateTasks += count ;
}
}
2025-12-04 09:22:45 +00:00
console . log (
` Incomplete: ${ incompleteTasks } , Completed: ${ completedTasks } , Custom states: ${ customStateTasks } `
) ;
console . log (
` Total tasks: ${
incompleteTasks +
completedTasks +
customStateTasks
} `
) ;
console . log (
` Progress: ${ Math . round (
( completedTasks /
( incompleteTasks +
completedTasks +
customStateTasks ) ) *
100
) } % `
) ;
2025-07-12 08:57:43 +00:00
} else {
console . log ( "Custom checkbox states are disabled" ) ;
2025-12-04 09:22:45 +00:00
const incompleteTasks = (
content . match ( /- \[ \]/g ) || [ ]
) . length ;
const completedTasks = (
content . match ( /- \[x\]/gi ) || [ ]
) . length ;
console . log (
` Legacy counting - Incomplete: ${ incompleteTasks } , Completed: ${ completedTasks } `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
new Notice (
"Custom checkbox state test completed. Check console for results."
) ;
2025-07-12 08:57:43 +00:00
} ) ;
} ,
} ) ;
// Add command to test card movement detection logic
this . addCommand ( {
id : "test-card-movement-detection" ,
name : "Test card movement detection logic" ,
callback : ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
if ( ! this . isKanbanBoard ( currentFile ) ) {
new Notice ( "Current file is not a Kanban board" ) ;
return ;
}
console . log ( "=== Card Movement Detection Test ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
2025-12-04 09:22:45 +00:00
console . log (
` Last Kanban content stored: ${ this . lastKanbanContent . has (
currentFile . path
) } `
) ;
console . log (
` Auto-synced: ${ this . autoSyncedFiles . has ( currentFile . path ) } `
) ;
console . log (
` isUpdatingFromKanban: ${ this . isUpdatingFromKanban } `
) ;
const lastUpdateTime = this . lastFileUpdateMap . get (
currentFile . path
) ;
2025-07-12 08:57:43 +00:00
if ( lastUpdateTime ) {
const timeSinceUpdate = Date . now ( ) - lastUpdateTime ;
console . log ( ` Time since last update: ${ timeSinceUpdate } ms ` ) ;
} else {
console . log ( ` No last update time recorded ` ) ;
}
// Test normalize function
const testCards = [
"- [x] [[Test Card]]" ,
"- [/] [[Another Card]]" ,
"- [ ] [[Todo Card]]" ,
2025-12-04 09:22:45 +00:00
"- [-] [[Cancelled Card]]" ,
2025-07-12 08:57:43 +00:00
] ;
console . log ( "Testing card normalization:" ) ;
2025-12-04 09:22:45 +00:00
testCards . forEach ( ( card ) = > {
const normalized =
this . normalizeCardContentForComparison ( card ) ;
2025-07-12 08:57:43 +00:00
console . log ( ` " ${ card } " -> " ${ normalized } " ` ) ;
} ) ;
2025-12-04 09:22:45 +00:00
new Notice (
"Card movement detection test completed. Check console for results."
) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to simulate card movement detection
this . addCommand ( {
id : "simulate-card-movement" ,
name : "Simulate card movement detection" ,
callback : async ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
if ( ! this . isKanbanBoard ( currentFile ) ) {
new Notice ( "Current file is not a Kanban board" ) ;
return ;
}
const oldContent = this . lastKanbanContent . get ( currentFile . path ) ;
if ( ! oldContent ) {
2025-12-04 09:22:45 +00:00
new Notice (
"No previous content stored. Try moving a card first."
) ;
2025-07-12 08:57:43 +00:00
return ;
}
const newContent = await this . app . vault . read ( currentFile ) ;
console . log ( "=== Simulating Card Movement Detection ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
try {
2025-12-04 09:22:45 +00:00
const actualMovements =
await this . detectActualCardMovements (
oldContent ,
newContent ,
currentFile
) ;
console . log (
` Detected ${ actualMovements . length } actual card movements: `
) ;
2025-07-12 08:57:43 +00:00
actualMovements . forEach ( ( movement , index ) = > {
2025-12-04 09:22:45 +00:00
console . log (
` ${ index + 1 } . " ${ movement . card . substring (
0 ,
40
) } . . . " from " $ { movement . oldColumn } " to " $ {
movement . newColumn
} " `
) ;
2025-07-12 08:57:43 +00:00
} ) ;
if ( actualMovements . length > 0 ) {
2025-12-04 09:22:45 +00:00
console . log (
"Testing updateCardCheckboxStatesInKanban with detected movements..."
) ;
const updatedContent =
await this . updateCardCheckboxStatesInKanban (
oldContent ,
newContent ,
currentFile ,
actualMovements
) ;
console . log (
` Content updated. Original length: ${ newContent . length } , Updated length: ${ updatedContent . length } `
) ;
2025-07-12 08:57:43 +00:00
if ( updatedContent !== newContent ) {
2025-12-04 09:22:45 +00:00
console . log (
"Content would be updated with checkbox state changes."
) ;
2025-07-12 08:57:43 +00:00
} else {
console . log ( "No checkbox state changes needed." ) ;
}
}
} catch ( error ) {
console . error ( "Error in simulation:" , error ) ;
}
2025-12-04 09:22:45 +00:00
new Notice (
"Card movement simulation completed. Check console for results."
) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to debug current mappings and board state
this . addCommand ( {
id : "debug-kanban-mappings-and-board-state" ,
name : "Debug Kanban mappings and board state" ,
callback : async ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
if ( ! this . isKanbanBoard ( currentFile ) ) {
new Notice ( "Current file is not a Kanban board" ) ;
return ;
}
console . log ( "=== Kanban Mappings and Board State Debug ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
2025-12-04 09:22:45 +00:00
console . log (
` enableCustomCheckboxStates: ${ this . settings . enableCustomCheckboxStates } `
) ;
2025-07-12 08:57:43 +00:00
// Show current mappings
console . log ( "Current checkbox mappings:" ) ;
2025-12-04 09:22:45 +00:00
this . settings . kanbanColumnCheckboxMappings . forEach (
( mapping , index ) = > {
console . log (
` ${ index + 1 } . " ${ mapping . columnName } " → " ${
mapping . checkboxState
} " `
) ;
}
) ;
2025-07-12 08:57:43 +00:00
// Parse and analyze current board state
const content = await this . app . vault . read ( currentFile ) ;
2025-12-04 09:22:45 +00:00
const kanban = await this . parseKanbanBoardContent (
content ,
currentFile
) ;
2025-07-12 08:57:43 +00:00
console . log ( "\nCurrent board state analysis:" ) ;
for ( const [ columnName , columnData ] of Object . entries ( kanban ) ) {
2025-12-04 09:22:45 +00:00
const expectedState =
this . getCheckboxStateForColumn ( columnName ) ;
console . log (
` \ nColumn: " ${ columnName } " (expected state: " ${ expectedState } ") `
) ;
2025-07-12 08:57:43 +00:00
console . log ( ` Total cards: ${ columnData . items . length } ` ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Count checkbox states in this column
2025-12-04 09:22:45 +00:00
const stateCount : { [ state : string ] : number } = { } ;
2025-07-12 08:57:43 +00:00
let correctCount = 0 ;
let incorrectCount = 0 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
columnData . items . forEach ( ( item , index ) = > {
const match = item . text . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
if ( match ) {
const currentState = ` [ ${ match [ 2 ] } ] ` ;
2025-12-04 09:22:45 +00:00
stateCount [ currentState ] =
( stateCount [ currentState ] || 0 ) + 1 ;
2025-07-12 08:57:43 +00:00
if ( currentState === expectedState ) {
correctCount ++ ;
} else {
incorrectCount ++ ;
2025-12-04 09:22:45 +00:00
console . log (
` ${ index } : " ${ item . text . substring (
0 ,
50
) } . . . " has " $ { currentState } " but should be " $ { expectedState } " `
) ;
2025-07-12 08:57:43 +00:00
}
} else {
2025-12-04 09:22:45 +00:00
console . log (
` ${ index } : " ${ item . text . substring (
0 ,
50
) } . . . " - no checkbox found `
) ;
2025-07-12 08:57:43 +00:00
}
} ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
console . log ( ` State distribution: ` , stateCount ) ;
2025-12-04 09:22:45 +00:00
console . log (
` Correct states: ${ correctCount } , Incorrect states: ${ incorrectCount } `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
new Notice (
"Kanban mappings and board state debug completed. Check console for results."
) ;
2025-07-12 08:57:43 +00:00
} ,
} ) ;
// Add command to manually trigger Kanban board change detection
this . addCommand ( {
id : "manual-trigger-kanban-change" ,
name : "Manually trigger Kanban board change detection" ,
callback : async ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
if ( ! this . isKanbanBoard ( currentFile ) ) {
new Notice ( "Current file is not a Kanban board" ) ;
return ;
}
console . log ( "=== Manual Kanban Change Trigger ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
try {
// Read current content
2025-12-04 09:22:45 +00:00
const currentContent = await this . app . vault . read (
currentFile
) ;
console . log (
` Current content length: ${ currentContent . length } `
) ;
2025-07-12 08:57:43 +00:00
// Trigger the change detection manually
2025-12-04 09:22:45 +00:00
await this . handleKanbanBoardChange (
currentFile ,
currentContent
) ;
new Notice (
"Manually triggered Kanban board change detection. Check console for logs."
) ;
2025-07-12 08:57:43 +00:00
} catch ( error ) {
console . error ( "Error in manual trigger:" , error ) ;
new Notice ( ` Error: ${ error . message } ` ) ;
}
} ,
} ) ;
// Add command to fix all checkbox states in current Kanban board
this . addCommand ( {
id : "fix-all-checkbox-states" ,
name : "Fix all checkbox states in current Kanban board" ,
callback : async ( ) = > {
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( ! currentFile ) {
new Notice ( "No active file" ) ;
return ;
}
if ( ! this . isKanbanBoard ( currentFile ) ) {
new Notice ( "Current file is not a Kanban board" ) ;
return ;
}
if ( ! this . settings . enableCustomCheckboxStates ) {
2025-12-04 09:22:45 +00:00
new Notice (
"Custom checkbox states are disabled. Enable them first in settings."
) ;
2025-07-12 08:57:43 +00:00
return ;
}
try {
// Set flag to prevent conflicts
this . isUpdatingFromKanban = true ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
console . log ( "=== Fixing All Checkbox States ===" ) ;
console . log ( ` File: ${ currentFile . path } ` ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Read current file content
const content = await this . app . vault . read ( currentFile ) ;
2025-12-04 09:22:45 +00:00
const kanban = await this . parseKanbanBoardContent (
content ,
currentFile
) ;
2025-07-12 08:57:43 +00:00
// Split content into lines for position-based replacement
2025-12-04 09:22:45 +00:00
const lines = content . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let totalChanges = 0 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Process each column and fix all cards
2025-12-04 09:22:45 +00:00
for ( const [ columnName , columnData ] of Object . entries (
kanban
) ) {
const targetCheckboxState =
this . getCheckboxStateForColumn ( columnName ) ;
console . log (
` Fixing column " ${ columnName } " to checkbox state " ${ targetCheckboxState } " ( ${ columnData . items . length } cards) `
) ;
2025-07-12 08:57:43 +00:00
// Update each card in this column
for ( const item of columnData . items ) {
2025-12-04 09:22:45 +00:00
const currentCheckboxMatch =
item . text . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const currentCheckboxState = currentCheckboxMatch
? ` [ ${ currentCheckboxMatch [ 2 ] } ] `
: null ;
2025-07-12 08:57:43 +00:00
// Only update if the current state is different from target state
if ( currentCheckboxState !== targetCheckboxState ) {
2025-12-04 09:22:45 +00:00
const updatedCardText =
this . updateCheckboxStateInCardText (
item . text ,
targetCheckboxState
) ;
2025-07-12 08:57:43 +00:00
if ( updatedCardText !== item . text ) {
2025-12-04 09:22:45 +00:00
const cardPosition =
this . findCardPositionInContent (
item . text ,
lines ,
columnName
) ;
2025-07-12 08:57:43 +00:00
if ( cardPosition !== - 1 ) {
// Replace only the specific card at the found position
2025-12-04 09:22:45 +00:00
const cardLines = item . text . split ( "\n" ) ;
const updatedCardLines =
updatedCardText . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
// Replace the card lines at the specific position
2025-12-04 09:22:45 +00:00
lines . splice (
cardPosition ,
cardLines . length ,
. . . updatedCardLines
) ;
2025-07-12 08:57:43 +00:00
totalChanges ++ ;
2025-12-04 09:22:45 +00:00
console . log (
` Fixed card: " ${ item . text . substring (
0 ,
30
) } . . . " from " $ { currentCheckboxState } " to " $ { targetCheckboxState } " `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Update the file if changes were made
if ( totalChanges > 0 ) {
2025-12-04 09:22:45 +00:00
const updatedContent = lines . join ( "\n" ) ;
await this . app . vault . modify (
currentFile ,
updatedContent
) ;
2025-07-12 08:57:43 +00:00
// Update stored content
2025-12-04 09:22:45 +00:00
this . lastKanbanContent . set (
currentFile . path ,
updatedContent
) ;
console . log (
` Fixed ${ totalChanges } checkbox states in ${ currentFile . basename } `
) ;
new Notice (
` Fixed ${ totalChanges } checkbox states in ${ currentFile . basename } `
) ;
2025-07-12 08:57:43 +00:00
} else {
2025-12-04 09:22:45 +00:00
console . log (
` No changes needed - all checkbox states are already correct `
) ;
2025-07-12 08:57:43 +00:00
new Notice ( "All checkbox states are already correct" ) ;
}
} catch ( error ) {
console . error ( "Error fixing checkbox states:" , error ) ;
2025-12-04 09:22:45 +00:00
new Notice (
` Error fixing checkbox states: ${ error . message } `
) ;
2025-07-12 08:57:43 +00:00
} finally {
// Reset flag
setTimeout ( ( ) = > {
this . isUpdatingFromKanban = false ;
} , 200 ) ;
}
} ,
} ) ;
2022-04-15 18:13:31 +00:00
}
2025-07-12 09:25:57 +00:00
/ * *
* Check for Dataview API availability and set up retry interval if not found
* This ensures the plugin can integrate with Dataview when it becomes available
* /
2025-03-16 09:26:27 +00:00
checkDataviewAPI() {
2025-03-20 11:32:16 +00:00
// Check immediately
2025-03-16 09:26:27 +00:00
this . dvAPI = getDataviewAPI ( this . app ) ;
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// If not found, set up interval to check again
2025-03-16 09:26:27 +00:00
if ( ! this . dvAPI ) {
this . dataviewCheckInterval = window . setInterval ( ( ) = > {
this . dvAPI = getDataviewAPI ( this . app ) ;
if ( this . dvAPI ) {
2025-03-20 11:32:16 +00:00
// If found, clear interval
2025-03-16 09:26:27 +00:00
if ( this . dataviewCheckInterval ) {
clearInterval ( this . dataviewCheckInterval ) ;
this . dataviewCheckInterval = null ;
}
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// Update sidebar if open
2025-03-16 09:26:27 +00:00
if ( this . sidebarView && this . lastActiveFile ) {
this . sidebarView . updateProgressBar ( this . lastActiveFile ) ;
}
}
2025-03-20 11:32:16 +00:00
} , 2000 ) ; // Check every 2 seconds
2025-03-16 09:26:27 +00:00
}
}
2025-07-12 09:25:57 +00:00
/ * *
* Update cached file content for comparison in future changes
* @param file File to cache content for
* /
2025-03-16 09:26:27 +00:00
async updateLastFileContent ( file : TFile ) {
if ( file ) {
this . lastFileContent = await this . app . vault . read ( file ) ;
}
}
async activateView() {
try {
const { workspace } = this . app ;
2025-03-16 19:20:48 +00:00
2025-12-04 09:22:45 +00:00
// Wait for layout to be ready before checking for existing views
2025-04-15 11:09:02 +00:00
workspace . onLayoutReady ( ( ) = > {
2025-12-04 09:22:45 +00:00
// Check for existing leaves after layout is ready
const leaves = workspace . getLeavesOfType ( "progress-tracker" ) ;
// If multiple views exist, detach duplicates and keep only the first one
if ( leaves . length > 1 ) {
this . logger . log (
` Found ${ leaves . length } progress-tracker views, removing duplicates `
) ;
// Keep the first leaf, detach the rest
for ( let i = 1 ; i < leaves . length ; i ++ ) {
leaves [ i ] . detach ( ) ;
}
// Reveal the remaining leaf
workspace . revealLeaf ( leaves [ 0 ] ) ;
return ;
}
// If view already exists, just reveal it
if ( leaves . length === 1 ) {
workspace . revealLeaf ( leaves [ 0 ] ) ;
return ;
}
// Otherwise, create a new leaf in the right sidebar (as user preference)
const leaf = workspace . getRightLeaf ( false ) ;
2025-04-15 11:09:02 +00:00
if ( leaf ) {
leaf . setViewState ( {
type : "progress-tracker" ,
active : true ,
} ) ;
2025-03-16 19:20:48 +00:00
2025-04-15 11:09:02 +00:00
// reveal the leaf
workspace . revealLeaf ( leaf ) ;
}
2025-04-15 11:25:00 +00:00
} ) ;
2025-03-16 09:26:27 +00:00
} catch ( error ) {
2025-03-16 19:20:48 +00:00
console . error ( "Error activating view:" , error ) ;
new Notice (
"Error activating Task Progress Bar view. Please try again later."
) ;
2025-03-16 09:26:27 +00:00
}
}
2025-07-12 09:25:57 +00:00
/ * *
* Cleanup all resources when plugin is unloaded
* Prevents memory leaks and ensures proper cleanup
* /
2025-03-16 09:26:27 +00:00
onunload() {
2025-07-12 09:25:57 +00:00
try {
// Clear interval if it exists
if ( this . dataviewCheckInterval ) {
clearInterval ( this . dataviewCheckInterval ) ;
this . dataviewCheckInterval = null ;
}
2025-03-16 19:20:48 +00:00
2025-07-12 09:25:57 +00:00
// Clear any in-memory data
if ( this . sidebarView ) {
this . sidebarView . clearCompletedFilesCache ( ) ;
}
// Clear all Maps and Sets to prevent memory leaks
this . lastKanbanContent . clear ( ) ;
this . autoSyncedFiles . clear ( ) ;
this . lastFileUpdateMap . clear ( ) ;
this . kanbanNormalizationDetector . clear ( ) ;
this . fileOperationLimiter . clear ( ) ;
// Reset flags
this . isUpdatingFromKanban = false ;
this . lastActiveFile = null ;
this . lastFileContent = "" ;
// Remove custom CSS styles
2025-12-04 09:22:45 +00:00
const existingStyle = document . getElementById (
"progress-tracker-max-tabs-height"
) ;
2025-07-12 09:25:57 +00:00
if ( existingStyle ) {
existingStyle . remove ( ) ;
}
2025-07-12 08:57:43 +00:00
2025-07-12 09:25:57 +00:00
this . logger . log ( "Plugin cleanup completed successfully" ) ;
} catch ( error ) {
console . error ( "Error during plugin cleanup:" , error ) ;
}
2025-07-12 08:57:43 +00:00
}
async loadSettings() {
this . settings = Object . assign (
{ } ,
DEFAULT_SETTINGS ,
await this . loadData ( )
) ;
}
async saveSettings() {
try {
await this . saveData ( this . settings ) ;
// Apply the max-height style whenever settings are saved
this . applyMaxTabsHeightStyle ( ) ;
} catch ( error ) {
console . error ( "Error saving settings:" , error ) ;
new Notice ( "Error saving settings. See console for details." ) ;
}
2022-04-15 18:13:31 +00:00
}
2025-07-12 09:25:57 +00:00
/ * *
* Safely apply max - height CSS style with input validation
* Prevents XSS by validating CSS values before DOM insertion
* /
2025-07-12 08:57:43 +00:00
applyMaxTabsHeightStyle() {
try {
// Remove any existing style element first
const existingStyle = document . getElementById (
"progress-tracker-max-tabs-height"
) ;
if ( existingStyle ) {
existingStyle . remove ( ) ;
}
2025-07-12 09:25:57 +00:00
// Validate CSS value to prevent XSS
const maxHeight = this . settings . maxTabsHeight ;
if ( ! this . isValidCSSValue ( maxHeight ) ) {
2025-12-04 09:22:45 +00:00
this . logger . error (
` Invalid CSS value for maxTabsHeight: ${ maxHeight } `
) ;
2025-07-12 09:25:57 +00:00
return ;
}
2025-07-12 08:57:43 +00:00
// Create a new style element
const style = document . createElement ( "style" ) ;
style . id = "progress-tracker-max-tabs-height" ;
// Target only workspace-tabs containing our plugin view
if ( ! this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
// Target only the progress-tracker view content, not the entire tabs container
// Use textContent instead of innerHTML for safety: .workspace-tabs.mod-top .workspace-tab-container:has(.progress-tracker-leaf) {
2025-07-12 08:57:43 +00:00
style . textContent = `
2025-12-04 09:22:45 +00:00
. workspace - tabs . mod - top . mod - top - right - space {
max - height : $ { maxHeight } ! important ;
}
` ;
2025-07-12 08:57:43 +00:00
// Add the style to the document head
document . head . appendChild ( style ) ;
}
2025-12-04 09:22:45 +00:00
this . logger . log (
` Applied max-tabs-height: ${ maxHeight } to Progress Tracker view `
) ;
2025-07-12 09:25:57 +00:00
} catch ( error ) {
this . logger . error ( "Error applying max tabs height style" , error ) ;
}
}
/ * *
* Validate CSS value to prevent XSS injection
* @param value CSS value to validate
* @returns true if value is safe to use
* /
private isValidCSSValue ( value : string ) : boolean {
2025-12-04 09:22:45 +00:00
if ( ! value || typeof value !== "string" ) return false ;
2025-07-12 09:25:57 +00:00
// Allow specific safe values
2025-12-04 09:22:45 +00:00
if ( value === "auto" || value === "none" ) return true ;
2025-07-12 09:25:57 +00:00
// Allow valid CSS length values (px, em, rem, vh, %)
const validCSSPattern = /^(\d+(\.\d+)?)(px|em|rem|vh|%)$/ ;
return validCSSPattern . test ( value . trim ( ) ) ;
}
/ * *
* Safe debug output for commands - only shows when debug is enabled
* @param title Debug section title
* @param data Debug data to display
* /
private debugOutput ( title : string , data : Record < string , any > ) : void {
if ( ! this . settings . showDebugInfo ) return ;
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
this . logger . log ( ` === ${ title } === ` ) ;
Object . entries ( data ) . forEach ( ( [ key , value ] ) = > {
2025-12-04 09:22:45 +00:00
this . logger . log (
` ${ key } : ${
typeof value === "object" ? JSON . stringify ( value ) : value
} `
) ;
2025-07-12 09:25:57 +00:00
} ) ;
}
/ * *
* Validate file before performing operations
* @param file File to validate
* @returns true if file is safe to operate on
* /
private isValidFile ( file : TFile ) : boolean {
if ( ! file ) return false ;
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
// Check if file path is safe (no path traversal)
2025-12-04 09:22:45 +00:00
if ( file . path . includes ( ".." ) || file . path . includes ( "//" ) ) {
2025-07-12 09:25:57 +00:00
this . logger . error ( ` Unsafe file path detected: ${ file . path } ` ) ;
return false ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
// Check if file is markdown
2025-12-04 09:22:45 +00:00
if ( ! file . path . endsWith ( ".md" ) ) {
2025-07-12 09:25:57 +00:00
this . logger . warn ( ` Non-markdown file: ${ file . path } ` ) ;
return false ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
// Check file size (prevent processing extremely large files)
2025-12-04 09:22:45 +00:00
if ( file . stat . size > 10 * 1024 * 1024 ) {
// 10MB limit
this . logger . error (
` File too large: ${ file . path } ( ${ file . stat . size } bytes) `
) ;
2025-07-12 09:25:57 +00:00
return false ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
return true ;
}
/ * *
* Validate content before processing
* @param content Content to validate
* @returns true if content is safe to process
* /
private isValidContent ( content : string ) : boolean {
2025-12-04 09:22:45 +00:00
if ( typeof content !== "string" ) return false ;
2025-07-12 09:25:57 +00:00
// Check content size
2025-12-04 09:22:45 +00:00
if ( content . length > 5 * 1024 * 1024 ) {
// 5MB limit
this . logger . error (
` Content too large: ${ content . length } characters `
) ;
2025-07-12 09:25:57 +00:00
return false ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
// Check for suspicious patterns that might indicate injection
const suspiciousPatterns = [
/<script[^>]*>/i ,
/javascript:/i ,
/data:text\/html/i ,
2025-12-04 09:22:45 +00:00
/vbscript:/i ,
2025-07-12 09:25:57 +00:00
] ;
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
for ( const pattern of suspiciousPatterns ) {
if ( pattern . test ( content ) ) {
this . logger . error ( ` Suspicious content pattern detected ` ) ;
return false ;
2025-07-12 08:57:43 +00:00
}
2025-07-12 09:25:57 +00:00
}
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
return true ;
}
/ * *
* Rate limiter for file operations
* /
private fileOperationLimiter = new Map < string , number > ( ) ;
private readonly FILE_OPERATION_DELAY = 100 ; // Minimum ms between operations per file
/ * *
* Check if file operation is rate limited
* @param filePath Path of file to check
* @returns true if operation should be allowed
* /
private checkRateLimit ( filePath : string ) : boolean {
const now = Date . now ( ) ;
const lastOperation = this . fileOperationLimiter . get ( filePath ) ;
2025-12-04 09:22:45 +00:00
if ( lastOperation && now - lastOperation < this . FILE_OPERATION_DELAY ) {
2025-07-12 09:25:57 +00:00
this . logger . warn ( ` Rate limited file operation: ${ filePath } ` ) ;
return false ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 09:25:57 +00:00
this . fileOperationLimiter . set ( filePath , now ) ;
return true ;
}
/ * *
* Standardized error handler for plugin operations
* @param error Error object or message
* @param context Context where error occurred
* @param showNotice Whether to show user notification
* /
2025-12-04 09:22:45 +00:00
private handleError (
error : Error | string ,
context : string ,
showNotice : boolean = false
) : void {
2025-07-12 09:25:57 +00:00
const errorMessage = error instanceof Error ? error.message : error ;
const fullMessage = ` ${ context } : ${ errorMessage } ` ;
2025-12-04 09:22:45 +00:00
this . logger . error (
fullMessage ,
error instanceof Error ? error : undefined
) ;
2025-07-12 09:25:57 +00:00
if ( showNotice ) {
new Notice ( ` Progress Tracker Error: ${ errorMessage } ` ) ;
}
}
/ * *
* Safe async operation wrapper with error handling
* @param operation Async operation to execute
* @param context Context description for error handling
* @param fallbackValue Value to return on error
* /
private async safeAsyncOperation < T > (
operation : ( ) = > Promise < T > ,
context : string ,
fallbackValue : T
) : Promise < T > {
try {
return await operation ( ) ;
2025-07-12 08:57:43 +00:00
} catch ( error ) {
2025-07-12 09:25:57 +00:00
this . handleError ( error as Error , context , false ) ;
return fallbackValue ;
2025-07-12 08:57:43 +00:00
}
}
/ * *
* Check if content changes are related to tasks
2025-07-12 09:25:57 +00:00
* Supports custom checkbox states like [ / ] , [ - ] , [ ~ ] , e t c .
* @param oldContent Previous file content
* @param newContent Current file content
* @returns true if task - related changes detected
2025-07-12 08:57:43 +00:00
* /
2025-12-04 09:22:45 +00:00
private hasTaskContentChanged (
oldContent : string ,
newContent : string
) : boolean {
2025-07-12 08:57:43 +00:00
// Split content into lines
2025-12-04 09:22:45 +00:00
const oldLines = oldContent . split ( "\n" ) ;
const newLines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
// Find task lines in both contents - support all checkbox states
2025-12-04 09:22:45 +00:00
const oldTasks = oldLines . filter ( ( line ) = >
line . trim ( ) . match ( /^[-*] \[[^\]]*\]/i )
) ;
const newTasks = newLines . filter ( ( line ) = >
line . trim ( ) . match ( /^[-*] \[[^\]]*\]/i )
) ;
2025-07-12 08:57:43 +00:00
// Compare task count
if ( oldTasks . length !== newTasks . length ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"Task count changed:" ,
oldTasks . length ,
"->" ,
newTasks . length
) ;
2025-07-12 08:57:43 +00:00
}
return true ;
}
// Compare each task
for ( let i = 0 ; i < oldTasks . length ; i ++ ) {
if ( oldTasks [ i ] !== newTasks [ i ] ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"Task content changed:" ,
oldTasks [ i ] ,
"->" ,
newTasks [ i ]
) ;
2025-07-12 08:57:43 +00:00
}
return true ;
}
}
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log ( "No task-related changes detected" ) ;
2025-07-12 08:57:43 +00:00
}
return false ;
}
/ * *
2025-07-12 09:25:57 +00:00
* Check if a file is a Kanban board
* Uses heuristics to detect Kanban structure and plugin metadata
* @param file File to check
* @returns true if file appears to be a Kanban board
2025-07-12 08:57:43 +00:00
* /
public isKanbanBoard ( file : TFile ) : boolean {
if ( ! this . sidebarView ) return false ;
return ( this . sidebarView as any ) . isKanbanBoard ( file ) ;
}
/ * *
* Handle changes in Kanban board files to detect card movements and update card checkbox states
* /
2025-12-04 09:22:45 +00:00
async handleKanbanBoardChange (
kanbanFile : TFile ,
newContent : string
) : Promise < void > {
2025-07-12 08:57:43 +00:00
try {
2025-07-12 09:25:57 +00:00
// Validate inputs
if ( ! this . isValidFile ( kanbanFile ) ) {
2025-12-04 09:22:45 +00:00
this . logger . error (
` Invalid file for Kanban board change: ${ kanbanFile ? . path } `
) ;
2025-07-12 09:25:57 +00:00
return ;
}
if ( ! this . isValidContent ( newContent ) ) {
this . logger . error ( ` Invalid content for Kanban board change ` ) ;
return ;
2025-07-12 08:57:43 +00:00
}
2025-07-12 09:25:57 +00:00
// Check rate limit
if ( ! this . checkRateLimit ( kanbanFile . path ) ) {
return ;
}
2025-12-04 09:22:45 +00:00
this . logger . log (
` handleKanbanBoardChange called for: ${ kanbanFile . path } `
) ;
this . logger . log (
` Settings - enableKanbanToFileSync: ${ this . settings . enableKanbanToFileSync } , enableCustomCheckboxStates: ${ this . settings . enableCustomCheckboxStates } `
) ;
2025-07-12 09:25:57 +00:00
2025-12-04 09:22:45 +00:00
if (
! this . settings . enableKanbanToFileSync ||
! this . settings . enableCustomCheckboxStates
) {
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log ( "Kanban sync disabled, skipping..." ) ;
2025-07-12 08:57:43 +00:00
}
return ;
}
const filePath = kanbanFile . path ;
const oldContent = this . lastKanbanContent . get ( filePath ) || "" ;
// CRITICAL FIX: Check if we're currently updating from auto-sync or other operations
if ( this . isUpdatingFromKanban ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"Skipping card movement detection - currently updating from auto-sync or other operations"
) ;
2025-07-12 08:57:43 +00:00
}
return ;
}
// CRITICAL FIX: Check if auto-sync recently ran on this file
if ( this . autoSyncedFiles . has ( filePath ) ) {
2025-12-04 09:22:45 +00:00
const timeSinceLastUpdate =
Date . now ( ) - ( this . lastFileUpdateMap . get ( filePath ) || 0 ) ;
if ( timeSinceLastUpdate < 2000 ) {
// If auto-sync ran within last 2 seconds
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Skipping card movement detection - auto-sync ran recently ( ${ timeSinceLastUpdate } ms ago) `
) ;
2025-07-12 08:57:43 +00:00
}
// Still update the stored content for next comparison
this . lastKanbanContent . set ( filePath , newContent ) ;
return ;
}
}
// Update the last file update timestamp
this . lastFileUpdateMap . set ( filePath , Date . now ( ) ) ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Old content length: ${ oldContent . length } , New content length: ${ newContent . length } `
) ;
2025-07-12 08:57:43 +00:00
}
// Store the new content for next comparison
this . lastKanbanContent . set ( filePath , newContent ) ;
// Skip if this is the first time we see this file
if ( ! oldContent ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` First time seeing Kanban board: ${ filePath } , storing content for next time `
) ;
2025-07-12 08:57:43 +00:00
}
return ;
}
// Skip if content is identical
if ( oldContent === newContent ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log ( "Content unchanged, skipping..." ) ;
2025-07-12 08:57:43 +00:00
}
return ;
}
// STEP 1: Detect actual card movements first
2025-12-04 09:22:45 +00:00
const actualCardMovements = await this . detectActualCardMovements (
oldContent ,
newContent ,
kanbanFile
) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Detected ${ actualCardMovements . length } actual card movements: ` ,
actualCardMovements
) ;
2025-07-12 08:57:43 +00:00
}
// STEP 2: Process legitimate card movements and update their checkbox states
let finalContent = newContent ;
if ( actualCardMovements . length > 0 ) {
2025-12-04 09:22:45 +00:00
finalContent = await this . updateCardCheckboxStatesInKanban (
oldContent ,
newContent ,
kanbanFile ,
actualCardMovements
) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Updated content for ${ actualCardMovements . length } card movements `
) ;
2025-07-12 08:57:43 +00:00
}
}
// STEP 3: Apply protection for non-moved cards (only if protection is enabled)
if ( this . settings . enableKanbanNormalizationProtection ) {
2025-12-04 09:22:45 +00:00
const isKanbanNormalization =
await this . detectKanbanNormalization (
kanbanFile ,
oldContent ,
finalContent ,
actualCardMovements
) ;
2025-07-12 08:57:43 +00:00
if ( isKanbanNormalization ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"Detected Kanban plugin normalization - protecting custom checkbox states for non-moved cards"
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
finalContent =
await this . protectCustomCheckboxStatesSelective (
kanbanFile ,
oldContent ,
finalContent ,
actualCardMovements
) ;
2025-07-12 08:57:43 +00:00
}
}
// STEP 4: Proactively sync all checkbox states to match column mappings
2025-12-04 09:22:45 +00:00
finalContent = await this . syncAllCheckboxStatesToMappings (
kanbanFile ,
finalContent
) ;
2025-07-12 08:57:43 +00:00
// STEP 5: Update the Kanban board file if content changed
if ( finalContent !== newContent ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Content will be updated. Original length: ${ newContent . length } , Final length: ${ finalContent . length } `
) ;
2025-07-12 08:57:43 +00:00
}
// Set flag to prevent infinite loops
this . isUpdatingFromKanban = true ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
await this . app . vault . modify ( kanbanFile , finalContent ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Update our stored content
this . lastKanbanContent . set ( filePath , finalContent ) ;
// Try to force refresh Kanban UI
await this . forceRefreshKanbanUI ( kanbanFile ) ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Successfully updated checkbox states in Kanban board: ${ kanbanFile . basename } `
) ;
2025-07-12 08:57:43 +00:00
}
// Reset flag after a short delay
setTimeout ( ( ) = > {
this . isUpdatingFromKanban = false ;
} , 300 ) ;
} else {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log ( "No checkbox state changes needed" ) ;
2025-07-12 08:57:43 +00:00
}
}
} catch ( error ) {
2025-07-12 09:25:57 +00:00
this . handleError ( error as Error , "handleKanbanBoardChange" , false ) ;
2025-07-12 08:57:43 +00:00
}
}
/ * *
* Detect actual card movements between columns ( not just checkbox state changes )
* Returns array of card movements with old and new column information
* /
private async detectActualCardMovements (
2025-12-04 09:22:45 +00:00
oldContent : string ,
newContent : string ,
2025-07-12 08:57:43 +00:00
kanbanFile : TFile
2025-12-04 09:22:45 +00:00
) : Promise <
Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} >
> {
2025-07-12 08:57:43 +00:00
try {
2025-12-04 09:22:45 +00:00
const movements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} > = [ ] ;
2025-07-12 08:57:43 +00:00
// Parse both old and new Kanban structures
2025-12-04 09:22:45 +00:00
const oldKanban = await this . parseKanbanBoardContent (
oldContent ,
kanbanFile
) ;
const newKanban = await this . parseKanbanBoardContent (
newContent ,
kanbanFile
) ;
2025-07-12 08:57:43 +00:00
// Create more precise card tracking with position information
2025-12-04 09:22:45 +00:00
const oldCardPositions = new Map <
string ,
Array < { column : string ; index : number ; originalText : string } >
> ( ) ;
const newCardPositions = new Map <
string ,
Array < { column : string ; index : number ; originalText : string } >
> ( ) ;
2025-07-12 08:57:43 +00:00
// Populate old card positions map
for ( const [ columnName , columnData ] of Object . entries ( oldKanban ) ) {
columnData . items . forEach ( ( item , index ) = > {
2025-12-04 09:22:45 +00:00
const normalizedCard =
this . normalizeCardContentForComparison ( item . text ) ;
2025-07-12 08:57:43 +00:00
if ( ! oldCardPositions . has ( normalizedCard ) ) {
oldCardPositions . set ( normalizedCard , [ ] ) ;
}
oldCardPositions . get ( normalizedCard ) ! . push ( {
column : columnName ,
index : index ,
2025-12-04 09:22:45 +00:00
originalText : item.text ,
2025-07-12 08:57:43 +00:00
} ) ;
} ) ;
}
// Populate new card positions and detect movements
for ( const [ columnName , columnData ] of Object . entries ( newKanban ) ) {
columnData . items . forEach ( ( item , index ) = > {
2025-12-04 09:22:45 +00:00
const normalizedCard =
this . normalizeCardContentForComparison ( item . text ) ;
2025-07-12 08:57:43 +00:00
if ( ! newCardPositions . has ( normalizedCard ) ) {
newCardPositions . set ( normalizedCard , [ ] ) ;
}
newCardPositions . get ( normalizedCard ) ! . push ( {
column : columnName ,
index : index ,
2025-12-04 09:22:45 +00:00
originalText : item.text ,
2025-07-12 08:57:43 +00:00
} ) ;
} ) ;
}
// Detect movements by comparing card distributions across columns
for ( const [ normalizedCard , newPositions ] of newCardPositions ) {
const oldPositions = oldCardPositions . get ( normalizedCard ) || [ ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check for cards that appear in new columns where they weren't before
for ( const newPos of newPositions ) {
2025-12-04 09:22:45 +00:00
const wasInThisColumn = oldPositions . some (
( oldPos ) = > oldPos . column === newPos . column
) ;
2025-07-12 08:57:43 +00:00
if ( ! wasInThisColumn && oldPositions . length > 0 ) {
// This card appears in a new column - it's a movement
// Find the most likely source column (the one that lost a card)
const oldColumnCounts = new Map < string , number > ( ) ;
const newColumnCounts = new Map < string , number > ( ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Count cards in each column
2025-12-04 09:22:45 +00:00
oldPositions . forEach ( ( pos ) = > {
oldColumnCounts . set (
pos . column ,
( oldColumnCounts . get ( pos . column ) || 0 ) + 1
) ;
2025-07-12 08:57:43 +00:00
} ) ;
2025-12-04 09:22:45 +00:00
newPositions . forEach ( ( pos ) = > {
newColumnCounts . set (
pos . column ,
( newColumnCounts . get ( pos . column ) || 0 ) + 1
) ;
2025-07-12 08:57:43 +00:00
} ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Find column that lost a card
for ( const [ oldColumn , oldCount ] of oldColumnCounts ) {
2025-12-04 09:22:45 +00:00
const newCount =
newColumnCounts . get ( oldColumn ) || 0 ;
2025-07-12 08:57:43 +00:00
if ( newCount < oldCount ) {
// This column lost a card - it's likely the source
movements . push ( {
card : normalizedCard ,
oldColumn : oldColumn ,
newColumn : newPos.column ,
2025-12-04 09:22:45 +00:00
cardIndex : newPos.index ,
2025-07-12 08:57:43 +00:00
} ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Detected card movement: " ${ normalizedCard . substring (
0 ,
30
) } . . . " from " $ { oldColumn } " to " $ {
newPos . column
} " `
) ;
2025-07-12 08:57:43 +00:00
}
break ; // Only record one movement per card instance
}
}
}
}
}
return movements ;
} catch ( error ) {
2025-12-04 09:22:45 +00:00
this . handleError (
error as Error ,
"detectActualCardMovements" ,
false
) ;
2025-07-12 08:57:43 +00:00
return [ ] ;
}
}
/ * *
* Normalize card content for comparison by removing checkbox states and extra whitespace
* This allows us to detect card movements regardless of checkbox state changes
* /
private normalizeCardContentForComparison ( cardContent : string ) : string {
return cardContent
2025-12-04 09:22:45 +00:00
. replace ( /^(\s*- )\[[^\]]*\](.*)$/gm , "$1$2" ) // Remove checkbox states
2025-07-12 08:57:43 +00:00
. trim ( ) ; // Remove extra whitespace
}
/ * *
* Update checkbox states in Kanban board based on card movements
* Uses position - based replacement to avoid affecting wrong cards
* /
private async updateCardCheckboxStatesInKanban (
2025-12-04 09:22:45 +00:00
oldContent : string ,
newContent : string ,
2025-07-12 08:57:43 +00:00
kanbanFile : TFile ,
2025-12-04 09:22:45 +00:00
actualCardMovements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} >
2025-07-12 08:57:43 +00:00
) : Promise < string > {
try {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log ( "Starting card checkbox state update process..." ) ;
console . log (
` Processing ${ actualCardMovements . length } actual card movements `
) ;
2025-07-12 08:57:43 +00:00
}
// If no actual card movements, return original content
if ( actualCardMovements . length === 0 ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"No actual card movements to process, returning original content"
) ;
2025-07-12 08:57:43 +00:00
}
return newContent ;
}
// Parse new Kanban structure to find cards to update
2025-12-04 09:22:45 +00:00
const newKanban = await this . parseKanbanBoardContent (
newContent ,
kanbanFile
) ;
2025-07-12 08:57:43 +00:00
// Split content into lines for position-based replacement
2025-12-04 09:22:45 +00:00
const lines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let changesFound = 0 ;
// Process only the cards that actually moved
for ( const movement of actualCardMovements ) {
2025-12-04 09:22:45 +00:00
const {
card : normalizedCard ,
oldColumn ,
newColumn ,
cardIndex ,
} = movement ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Processing movement: " ${ normalizedCard . substring (
0 ,
30
) } . . . " from " $ { oldColumn } " to " $ { newColumn } " `
) ;
2025-07-12 08:57:43 +00:00
}
// Find the actual card in the new content using the specific index
const targetColumn = newKanban [ newColumn ] ;
if ( ! targetColumn ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Target column " ${ newColumn } " not found in new content `
) ;
2025-07-12 08:57:43 +00:00
}
continue ;
}
// Use the specific card index to get the exact card that moved
if ( cardIndex >= targetColumn . items . length ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Card index ${ cardIndex } out of range for column " ${ newColumn } " (has ${ targetColumn . items . length } items) `
) ;
2025-07-12 08:57:43 +00:00
}
continue ;
}
const foundCard = targetColumn . items [ cardIndex ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Double-check that this is the right card
2025-12-04 09:22:45 +00:00
const itemNormalized = this . normalizeCardContentForComparison (
foundCard . text
) ;
2025-07-12 08:57:43 +00:00
if ( itemNormalized !== normalizedCard ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Card at index ${ cardIndex } doesn't match expected content. Expected: " ${ normalizedCard } ", Found: " ${ itemNormalized } " `
) ;
2025-07-12 08:57:43 +00:00
}
continue ;
}
// Update the checkbox state for this card
2025-12-04 09:22:45 +00:00
const targetCheckboxState =
this . getCheckboxStateForColumn ( newColumn ) ;
const updatedCardText = this . updateCheckboxStateInCardText (
foundCard . text ,
targetCheckboxState
) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Target checkbox state: " ${ targetCheckboxState } " `
) ;
2025-07-12 08:57:43 +00:00
console . log ( ` Original card: ${ foundCard . text } ` ) ;
console . log ( ` Updated card: ${ updatedCardText } ` ) ;
}
// Use position-based replacement to update only this specific card
if ( updatedCardText !== foundCard . text ) {
// Use the specific card index to find the exact position in the content
2025-12-04 09:22:45 +00:00
const cardPosition = this . findCardPositionByIndex (
lines ,
newColumn ,
cardIndex
) ;
2025-07-12 08:57:43 +00:00
if ( cardPosition !== - 1 ) {
// Replace only the specific card at the found position
2025-12-04 09:22:45 +00:00
const cardLines = foundCard . text . split ( "\n" ) ;
const updatedCardLines = updatedCardText . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
// Replace the card lines at the specific position
2025-12-04 09:22:45 +00:00
lines . splice (
cardPosition ,
cardLines . length ,
. . . updatedCardLines
) ;
2025-07-12 08:57:43 +00:00
changesFound ++ ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Successfully updated card checkbox state at position ${ cardPosition } (index ${ cardIndex } ) from " ${ oldColumn } " to " ${ newColumn } ": ${ targetCheckboxState } `
) ;
2025-07-12 08:57:43 +00:00
}
} else {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Could not find position for card at index ${ cardIndex } in column " ${ newColumn } " `
) ;
2025-07-12 08:57:43 +00:00
}
}
} else {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` No changes needed for card (already has correct checkbox state) `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Card checkbox update complete. Changes found: ${ changesFound } out of ${ actualCardMovements . length } movements `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
return lines . join ( "\n" ) ;
2025-07-12 08:57:43 +00:00
} catch ( error ) {
2025-12-04 09:22:45 +00:00
this . handleError (
error as Error ,
"updateCardCheckboxStatesInKanban" ,
false
) ;
2025-07-12 08:57:43 +00:00
return newContent ; // Return original content if there's an error
}
}
/ * *
* Parse Kanban board content into structure ( simplified implementation )
* /
private async parseKanbanBoardContent (
2025-12-04 09:22:45 +00:00
content : string ,
2025-07-12 08:57:43 +00:00
file : TFile
) : Promise < Record < string , { items : Array < { text : string } > } >> {
const kanban : Record < string , { items : Array < { text : string } > } > = { } ;
try {
if ( this . settings . showDebugInfo ) {
console . log ( ` Parsing Kanban content for: ${ file . path } ` ) ;
}
// Split content into lines
2025-12-04 09:22:45 +00:00
const lines = content . split ( "\n" ) ;
let currentColumn = "" ;
2025-07-12 08:57:43 +00:00
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
// Check for column header (## Column Name)
2025-12-04 09:22:45 +00:00
if ( line . startsWith ( "## " ) ) {
2025-07-12 08:57:43 +00:00
currentColumn = line . substring ( 3 ) . trim ( ) ;
if ( ! kanban [ currentColumn ] ) {
kanban [ currentColumn ] = { items : [ ] } ;
}
if ( this . settings . showDebugInfo ) {
console . log ( ` Found column: ${ currentColumn } ` ) ;
}
continue ;
}
// Check for list item (starts with "- ")
2025-12-04 09:22:45 +00:00
if ( currentColumn && line . trim ( ) . startsWith ( "- " ) ) {
2025-07-12 08:57:43 +00:00
// Get complete card content including sub-items
let cardText = line ;
let j = i + 1 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Include indented sub-items
2025-12-04 09:22:45 +00:00
while (
j < lines . length &&
( lines [ j ] . startsWith ( " " ) ||
lines [ j ] . startsWith ( "\t" ) ||
lines [ j ] . trim ( ) === "" )
) {
cardText += "\n" + lines [ j ] ;
2025-07-12 08:57:43 +00:00
j ++ ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
kanban [ currentColumn ] . items . push ( { text : cardText } ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found card in ${ currentColumn } : ${ cardText . substring (
0 ,
50
) } . . . `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Skip the lines we already processed
i = j - 1 ;
}
}
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Parsed ${ Object . keys ( kanban ) . length } columns: ` ,
Object . keys ( kanban )
) ;
2025-07-12 08:57:43 +00:00
Object . entries ( kanban ) . forEach ( ( [ col , data ] ) = > {
console . log ( ` ${ col } : ${ data . items . length } items ` ) ;
data . items . forEach ( ( item , index ) = > {
2025-12-04 09:22:45 +00:00
console . log (
` ${ index } : ${ item . text . substring ( 0 , 50 ) } ... `
) ;
2025-07-12 08:57:43 +00:00
} ) ;
} ) ;
}
} catch ( error ) {
2025-12-04 09:22:45 +00:00
console . error ( "Error parsing Kanban content:" , error ) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . error ( "Error details:" , error ) ;
2025-07-12 08:57:43 +00:00
}
}
return kanban ;
}
/ * *
* Find which column a card is in within a Kanban structure
* /
private findCardInKanban (
2025-12-04 09:22:45 +00:00
cardText : string ,
2025-07-12 08:57:43 +00:00
kanban : Record < string , { items : Array < { text : string } > } >
) : string | null {
const trimmedCardText = cardText . trim ( ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
for ( const [ columnName , columnData ] of Object . entries ( kanban ) ) {
for ( const item of columnData . items ) {
const trimmedItemText = item . text . trim ( ) ;
if ( trimmedItemText === trimmedCardText ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found exact match for card " ${ trimmedCardText . substring (
0 ,
30
) } . . . " in column " $ { columnName } " `
) ;
2025-07-12 08:57:43 +00:00
}
return columnName ;
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Try fuzzy matching if exact match fails
for ( const [ columnName , columnData ] of Object . entries ( kanban ) ) {
for ( const item of columnData . items ) {
const trimmedItemText = item . text . trim ( ) ;
// Check if the core content matches (ignoring potential whitespace differences)
if ( this . areCardsEquivalent ( trimmedCardText , trimmedItemText ) ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found fuzzy match for card " ${ trimmedCardText . substring (
0 ,
30
) } . . . " in column " $ { columnName } " `
) ;
2025-07-12 08:57:43 +00:00
}
return columnName ;
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` No match found for card " ${ trimmedCardText . substring (
0 ,
30
) } . . . " in any column `
) ;
2025-07-12 08:57:43 +00:00
}
return null ;
}
/ * *
* Check if two cards are equivalent ( accounting for minor formatting differences )
* /
private areCardsEquivalent ( card1 : string , card2 : string ) : boolean {
// Extract the main link content from both cards
const link1 = this . extractMainLinkFromCard ( card1 ) ;
const link2 = this . extractMainLinkFromCard ( card2 ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
return ! ! ( link1 && link2 && link1 === link2 ) ;
}
/ * *
* Extract the main link content from a card
* /
private extractMainLinkFromCard ( cardText : string ) : string | null {
// Look for [[link]] pattern
const obsidianMatch = cardText . match ( /\[\[([^\]]+)\]\]/ ) ;
if ( obsidianMatch ) {
return obsidianMatch [ 1 ] ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Look for [text](url) pattern
const markdownMatch = cardText . match ( /\[([^\]]+)\]\(([^)]+)\)/ ) ;
if ( markdownMatch ) {
return markdownMatch [ 2 ] ; // Return the URL part
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
return null ;
}
/ * *
* Extract Obsidian - style links from content ( reused logic )
* /
2025-12-04 09:22:45 +00:00
private extractObsidianLinks (
content : string
) : Array < { path : string ; alias? : string } > {
const links : Array < { path : string ; alias? : string } > = [ ] ;
2025-07-12 08:57:43 +00:00
const linkPattern = /\[\[(.*?)\]\]/g ;
let match ;
while ( ( match = linkPattern . exec ( content ) ) !== null ) {
const [ _ , linkContent ] = match ;
2025-12-04 09:22:45 +00:00
const [ path , alias ] = linkContent . split ( "|" ) . map ( ( s ) = > s . trim ( ) ) ;
2025-07-12 08:57:43 +00:00
links . push ( { path , alias } ) ;
}
return links ;
}
/ * *
* Extract Markdown - style links from content ( reused logic )
* /
2025-12-04 09:22:45 +00:00
private extractMarkdownLinks (
content : string
) : Array < { text : string ; url : string } > {
const links : Array < { text : string ; url : string } > = [ ] ;
2025-07-12 08:57:43 +00:00
const linkPattern = /\[(.*?)\]\((.*?)\)/g ;
let match ;
while ( ( match = linkPattern . exec ( content ) ) !== null ) {
const [ _ , text , url ] = match ;
links . push ( { text : text.trim ( ) , url : url.trim ( ) } ) ;
}
return links ;
}
/ * *
* Get checkbox state for a specific kanban column ( reused logic )
* /
private getCheckboxStateForColumn ( columnName : string ) : string {
if ( ! this . settings . enableCustomCheckboxStates ) {
return "[ ]" ; // Default unchecked state
}
const mapping = this . settings . kanbanColumnCheckboxMappings . find (
2025-12-04 09:22:45 +00:00
( m ) = > m . columnName . toLowerCase ( ) === columnName . toLowerCase ( )
2025-07-12 08:57:43 +00:00
) ;
return mapping ? mapping . checkboxState : "[ ]" ;
}
/ * *
* Find the exact position of a card within content lines under a specific column
* /
2025-12-04 09:22:45 +00:00
private findCardPositionInContent (
cardText : string ,
lines : string [ ] ,
targetColumn : string
) : number {
let currentColumn = "" ;
2025-07-12 08:57:43 +00:00
let inTargetColumn = false ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check for column header
2025-12-04 09:22:45 +00:00
if ( line . startsWith ( "## " ) ) {
2025-07-12 08:57:43 +00:00
currentColumn = line . substring ( 3 ) . trim ( ) ;
2025-12-04 09:22:45 +00:00
inTargetColumn =
currentColumn . toLowerCase ( ) === targetColumn . toLowerCase ( ) ;
2025-07-12 08:57:43 +00:00
continue ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Only check for cards when we're in the target column
2025-12-04 09:22:45 +00:00
if ( inTargetColumn && line . trim ( ) . startsWith ( "- " ) ) {
2025-07-12 08:57:43 +00:00
// Get complete card content including sub-items
let completeCardText = line ;
let j = i + 1 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Include indented sub-items
2025-12-04 09:22:45 +00:00
while (
j < lines . length &&
( lines [ j ] . startsWith ( " " ) ||
lines [ j ] . startsWith ( "\t" ) ||
lines [ j ] . trim ( ) === "" )
) {
completeCardText += "\n" + lines [ j ] ;
2025-07-12 08:57:43 +00:00
j ++ ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check if this matches our target card
if ( completeCardText . trim ( ) === cardText . trim ( ) ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found card at position ${ i } in column " ${ targetColumn } " `
) ;
2025-07-12 08:57:43 +00:00
}
return i ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Skip the lines we already processed
i = j - 1 ;
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
console . log ( ` Card not found in column " ${ targetColumn } " ` ) ;
}
return - 1 ;
}
/ * *
* Find the exact position of a card by its index within a specific column
* This is more precise than content matching for avoiding duplicates
* /
2025-12-04 09:22:45 +00:00
private findCardPositionByIndex (
lines : string [ ] ,
targetColumn : string ,
cardIndex : number
) : number {
let currentColumn = "" ;
2025-07-12 08:57:43 +00:00
let inTargetColumn = false ;
let cardCount = 0 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check for column header
2025-12-04 09:22:45 +00:00
if ( line . startsWith ( "## " ) ) {
2025-07-12 08:57:43 +00:00
currentColumn = line . substring ( 3 ) . trim ( ) ;
2025-12-04 09:22:45 +00:00
inTargetColumn =
currentColumn . toLowerCase ( ) === targetColumn . toLowerCase ( ) ;
2025-07-12 08:57:43 +00:00
cardCount = 0 ; // Reset card count for new column
continue ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Only check for cards when we're in the target column
2025-12-04 09:22:45 +00:00
if ( inTargetColumn && line . trim ( ) . startsWith ( "- " ) ) {
2025-07-12 08:57:43 +00:00
// Check if this is the card we're looking for
if ( cardCount === cardIndex ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found card at position ${ i } (index ${ cardIndex } ) in column " ${ targetColumn } " `
) ;
2025-07-12 08:57:43 +00:00
}
return i ;
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
cardCount ++ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Skip sub-items to avoid counting them as separate cards
let j = i + 1 ;
2025-12-04 09:22:45 +00:00
while (
j < lines . length &&
( lines [ j ] . startsWith ( " " ) ||
lines [ j ] . startsWith ( "\t" ) ||
lines [ j ] . trim ( ) === "" )
) {
2025-07-12 08:57:43 +00:00
j ++ ;
}
i = j - 1 ;
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Card at index ${ cardIndex } not found in column " ${ targetColumn } " (found ${ cardCount } cards total) `
) ;
2025-07-12 08:57:43 +00:00
}
return - 1 ;
}
/ * *
* Update checkbox state in a single card text
* Only updates the main card checkbox , preserving sub - items and nested checkboxes
* /
2025-12-04 09:22:45 +00:00
private updateCheckboxStateInCardText (
cardText : string ,
targetCheckboxState : string
) : string {
2025-07-12 08:57:43 +00:00
// Split content into lines to process only the first line (main card)
2025-12-04 09:22:45 +00:00
const lines = cardText . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
if ( lines . length === 0 ) return cardText ;
// Pattern to match various checkbox states: - [ ], - [x], - [/], - [>], etc.
// Remove global flag to only match once per line
const checkboxPattern = /^(\s*[-*] )\[[^\]]*\](.*)$/ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Only update the first line if it matches the pattern (main card line)
if ( checkboxPattern . test ( lines [ 0 ] ) ) {
2025-12-04 09:22:45 +00:00
lines [ 0 ] = lines [ 0 ] . replace (
checkboxPattern ,
( match , prefix , suffix ) = > {
return ` ${ prefix } ${ targetCheckboxState } ${ suffix } ` ;
}
) ;
2025-07-12 08:57:43 +00:00
}
// Join lines back together, preserving sub-items unchanged
2025-12-04 09:22:45 +00:00
return lines . join ( "\n" ) ;
2025-07-12 08:57:43 +00:00
}
/ * *
* Count tasks with different checkbox states
* /
2025-12-04 09:22:45 +00:00
private countTasksByCheckboxState ( content : string ) : {
[ state : string ] : number ;
} {
2025-07-12 08:57:43 +00:00
const taskCounts : { [ state : string ] : number } = { } ;
2025-12-04 09:22:45 +00:00
const lines = content . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
for ( const line of lines ) {
const match = line . trim ( ) . match ( /^- \[([^\]]*)\]/ ) ;
if ( match ) {
const state = match [ 1 ] ;
taskCounts [ state ] = ( taskCounts [ state ] || 0 ) + 1 ;
}
}
return taskCounts ;
}
/ * *
* NEW : Detect if a change is caused by Kanban plugin normalization
* Returns true if this appears to be a normalization rather than user - intended change
* /
private async detectKanbanNormalization (
2025-12-04 09:22:45 +00:00
kanbanFile : TFile ,
oldContent : string ,
2025-07-12 08:57:43 +00:00
newContent : string ,
2025-12-04 09:22:45 +00:00
knownMovements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} > = [ ]
2025-07-12 08:57:43 +00:00
) : Promise < boolean > {
try {
const filePath = kanbanFile . path ;
const now = Date . now ( ) ;
// Initialize detector state if not exists
if ( ! this . kanbanNormalizationDetector . has ( filePath ) ) {
this . kanbanNormalizationDetector . set ( filePath , {
preChangeCheckpoints : new Map ( ) ,
lastKanbanUIInteraction : 0 ,
2025-12-04 09:22:45 +00:00
pendingNormalizationCheck : null ,
2025-07-12 08:57:43 +00:00
} ) ;
}
const detector = this . kanbanNormalizationDetector . get ( filePath ) ! ;
// Analyze the pattern of changes first
2025-12-04 09:22:45 +00:00
const normalizationPatterns =
this . analyzeCheckboxNormalizationPatterns (
oldContent ,
newContent
) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
// Enhanced detection logic - focus on content patterns rather than timing
const hasUnwantedNormalization =
this . detectUnwantedKanbanNormalization (
oldContent ,
newContent ,
knownMovements
) ;
2025-07-12 08:57:43 +00:00
// Update interaction timestamp for future detections
detector . lastKanbanUIInteraction = now ;
if ( this . settings . showDebugInfo ) {
console . log ( ` Kanban normalization analysis for ${ filePath } : ` , {
normalizationPatterns ,
hasUnwantedNormalization ,
2025-12-04 09:22:45 +00:00
contentLengthSame : oldContent.length === newContent . length ,
2025-07-12 08:57:43 +00:00
} ) ;
}
// Return true if this looks like unwanted Kanban normalization
2025-12-04 09:22:45 +00:00
return (
hasUnwantedNormalization ||
normalizationPatterns . hasNormalization
) ;
2025-07-12 08:57:43 +00:00
} catch ( error ) {
console . error ( "Error detecting Kanban normalization:" , error ) ;
return false ;
}
}
/ * *
* NEW : Detect unwanted Kanban normalization based on content patterns
* /
private detectUnwantedKanbanNormalization (
2025-12-04 09:22:45 +00:00
oldContent : string ,
2025-07-12 08:57:43 +00:00
newContent : string ,
2025-12-04 09:22:45 +00:00
knownMovements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} > = [ ]
2025-07-12 08:57:43 +00:00
) : boolean {
// Check if this is a case where custom states are being converted to [x] inappropriately
2025-12-04 09:22:45 +00:00
const oldLines = oldContent . split ( "\n" ) ;
const newLines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
// Look for patterns where custom states in non-completed columns get converted to [x]
for ( let i = 0 ; i < Math . min ( oldLines . length , newLines . length ) ; i ++ ) {
const oldLine = oldLines [ i ] ;
const newLine = newLines [ i ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Find what column this line is in
2025-12-04 09:22:45 +00:00
let currentColumn = "" ;
2025-07-12 08:57:43 +00:00
for ( let j = i ; j >= 0 ; j -- ) {
2025-12-04 09:22:45 +00:00
if ( oldLines [ j ] . startsWith ( "## " ) ) {
2025-07-12 08:57:43 +00:00
currentColumn = oldLines [ j ] . substring ( 3 ) . trim ( ) ;
break ;
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check for unwanted normalization
const oldMatch = oldLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const newMatch = newLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( oldMatch && newMatch && currentColumn ) {
const oldState = oldMatch [ 2 ] ;
const newState = newMatch [ 2 ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Get expected state for this column
2025-12-04 09:22:45 +00:00
const expectedState =
this . getCheckboxStateForColumn ( currentColumn ) ;
const expectedStateChar = expectedState . replace ( /[\[\]]/g , "" ) ;
2025-07-12 08:57:43 +00:00
// Check if this change is part of a legitimate movement
2025-12-04 09:22:45 +00:00
const isLegitimateMovement = knownMovements . some (
( movement ) = >
movement . newColumn . toLowerCase ( ) ===
currentColumn . toLowerCase ( )
2025-07-12 08:57:43 +00:00
) ;
// Detect unwanted conversion: custom state → [x] when it should be something else
// BUT only if it's not part of a legitimate movement
2025-12-04 09:22:45 +00:00
if (
oldState === expectedStateChar && // old state was correct for column
newState === "x" && // new state is [x]
expectedStateChar !== "x" && // but column shouldn't be [x]
! isLegitimateMovement
) {
// and it's not a legitimate movement
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Detected unwanted normalization in column " ${ currentColumn } ": [ ${ oldState } ] → [ ${ newState } ] (expected: ${ expectedState } ) `
) ;
2025-07-12 08:57:43 +00:00
}
return true ;
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
return false ;
}
/ * *
* NEW : Analyze checkbox normalization patterns
* /
2025-12-04 09:22:45 +00:00
private analyzeCheckboxNormalizationPatterns (
oldContent : string ,
newContent : string
) : {
2025-07-12 08:57:43 +00:00
hasNormalization : boolean ;
2025-12-04 09:22:45 +00:00
normalizedStates : Array < { line : number ; from : string ; to : string } > ;
2025-07-12 08:57:43 +00:00
} {
2025-12-04 09:22:45 +00:00
const oldLines = oldContent . split ( "\n" ) ;
const newLines = newContent . split ( "\n" ) ;
const normalizedStates : Array < {
line : number ;
from : string ;
to : string ;
} > = [ ] ;
2025-07-12 08:57:43 +00:00
for ( let i = 0 ; i < Math . min ( oldLines . length , newLines . length ) ; i ++ ) {
const oldLine = oldLines [ i ] ;
const newLine = newLines [ i ] ;
// Check if this line had a custom checkbox state that got normalized
const oldCheckboxMatch = oldLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const newCheckboxMatch = newLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
if ( oldCheckboxMatch && newCheckboxMatch ) {
const oldState = oldCheckboxMatch [ 2 ] ;
const newState = newCheckboxMatch [ 2 ] ;
// Detect normalization: custom state → standard state
2025-12-04 09:22:45 +00:00
if (
oldState !== newState &&
oldState !== " " &&
oldState !== "x" && // old was custom
( newState === "x" || newState === " " )
) {
// new is standard
2025-07-12 08:57:43 +00:00
normalizedStates . push ( {
line : i ,
from : ` [ ${ oldState } ] ` ,
2025-12-04 09:22:45 +00:00
to : ` [ ${ newState } ] ` ,
2025-07-12 08:57:43 +00:00
} ) ;
}
}
}
return {
hasNormalization : normalizedStates.length > 0 ,
2025-12-04 09:22:45 +00:00
normalizedStates ,
2025-07-12 08:57:43 +00:00
} ;
}
/ * *
* NEW : Protect custom checkbox states from unwanted normalization
* Restores custom states while preserving legitimate changes
* /
private async protectCustomCheckboxStates (
2025-12-04 09:22:45 +00:00
kanbanFile : TFile ,
oldContent : string ,
2025-07-12 08:57:43 +00:00
newContent : string
) : Promise < void > {
try {
const filePath = kanbanFile . path ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Analyze what was normalized
2025-12-04 09:22:45 +00:00
const analysis = this . analyzeCheckboxNormalizationPatterns (
oldContent ,
newContent
) ;
2025-07-12 08:57:43 +00:00
if ( ! analysis . hasNormalization ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"No normalization detected, no protection needed"
) ;
2025-07-12 08:57:43 +00:00
}
return ;
}
// Create protected content by restoring custom states
const protectedContent = this . restoreCustomCheckboxStates (
2025-12-04 09:22:45 +00:00
oldContent ,
newContent ,
2025-07-12 08:57:43 +00:00
analysis . normalizedStates ,
kanbanFile
) ;
if ( protectedContent !== newContent ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Protecting ${ analysis . normalizedStates . length } custom checkbox states from normalization `
) ;
analysis . normalizedStates . forEach ( ( state ) = > {
console . log (
` Line ${ state . line } : ${ state . from } → ${ state . to } (restoring ${ state . from } ) `
) ;
2025-07-12 08:57:43 +00:00
} ) ;
}
// Set flag to prevent infinite loops
this . isUpdatingFromKanban = true ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Apply protection
await this . app . vault . modify ( kanbanFile , protectedContent ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Update stored content
this . lastKanbanContent . set ( filePath , protectedContent ) ;
// Reset flag after delay
setTimeout ( ( ) = > {
this . isUpdatingFromKanban = false ;
} , 300 ) ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Successfully protected custom checkbox states in: ${ kanbanFile . basename } `
) ;
2025-07-12 08:57:43 +00:00
}
}
} catch ( error ) {
console . error ( "Error protecting custom checkbox states:" , error ) ;
if ( this . settings . showDebugInfo ) {
console . error ( "Protection error details:" , error ) ;
}
}
}
/ * *
* NEW : Selective protection that only protects non - moved cards from unwanted normalization
* Allows legitimate checkbox state changes for moved cards while protecting others
* /
private async protectCustomCheckboxStatesSelective (
2025-12-04 09:22:45 +00:00
kanbanFile : TFile ,
oldContent : string ,
2025-07-12 08:57:43 +00:00
newContent : string ,
2025-12-04 09:22:45 +00:00
knownMovements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} >
2025-07-12 08:57:43 +00:00
) : Promise < string > {
try {
// Analyze what was normalized
2025-12-04 09:22:45 +00:00
const analysis = this . analyzeCheckboxNormalizationPatterns (
oldContent ,
newContent
) ;
2025-07-12 08:57:43 +00:00
if ( ! analysis . hasNormalization ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"No normalization detected, no selective protection needed"
) ;
2025-07-12 08:57:43 +00:00
}
return newContent ;
}
// Filter out normalizations that correspond to legitimate movements
2025-12-04 09:22:45 +00:00
const legitimateNormalizations =
this . filterLegitimateNormalizations (
analysis . normalizedStates ,
knownMovements ,
newContent ,
kanbanFile
) ;
2025-07-12 08:57:43 +00:00
// Only protect states that are NOT part of legitimate movements
2025-12-04 09:22:45 +00:00
const statesNeedingProtection = analysis . normalizedStates . filter (
( state : { line : number ; from : string ; to : string } ) = >
! legitimateNormalizations . includes ( state )
2025-07-12 08:57:43 +00:00
) ;
if ( statesNeedingProtection . length === 0 ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
"All normalizations are legitimate movements, no protection needed"
) ;
2025-07-12 08:57:43 +00:00
}
return newContent ;
}
// Create protected content by restoring only non-legitimate changes
const protectedContent = this . restoreCustomCheckboxStates (
2025-12-04 09:22:45 +00:00
oldContent ,
newContent ,
2025-07-12 08:57:43 +00:00
statesNeedingProtection ,
kanbanFile
) ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Selectively protecting ${ statesNeedingProtection . length } out of ${ analysis . normalizedStates . length } normalized states `
) ;
console . log (
` Allowing ${ legitimateNormalizations . length } legitimate state changes from card movements `
) ;
statesNeedingProtection . forEach (
( state : { line : number ; from : string ; to : string } ) = > {
console . log (
` Protecting line ${ state . line } : ${ state . from } → ${ state . to } (restoring ${ state . from } ) `
) ;
}
) ;
legitimateNormalizations . forEach (
( state : { line : number ; from : string ; to : string } ) = > {
console . log (
` Allowing line ${ state . line } : ${ state . from } → ${ state . to } (legitimate movement) `
) ;
}
) ;
2025-07-12 08:57:43 +00:00
}
return protectedContent ;
} catch ( error ) {
console . error ( "Error in selective protection:" , error ) ;
if ( this . settings . showDebugInfo ) {
console . error ( "Selective protection error details:" , error ) ;
}
return newContent ; // Return original content if there's an error
}
}
/ * *
* NEW : Restore custom checkbox states while preserving other changes
* /
private restoreCustomCheckboxStates (
2025-12-04 09:22:45 +00:00
oldContent : string ,
newContent : string ,
normalizedStates : Array < { line : number ; from : string ; to : string } > ,
2025-07-12 08:57:43 +00:00
kanbanFile : TFile
) : string {
2025-12-04 09:22:45 +00:00
const lines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let restoredCount = 0 ;
for ( const normalizedState of normalizedStates ) {
const lineIndex = normalizedState . line ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( lineIndex < lines . length ) {
const line = lines [ lineIndex ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Only restore if the column mapping supports this custom state
const columnName = this . findLineColumn ( line , lines , lineIndex ) ;
if ( columnName ) {
2025-12-04 09:22:45 +00:00
const expectedState =
this . getCheckboxStateForColumn ( columnName ) ;
2025-07-12 08:57:43 +00:00
// If the old state matches what we expect for this column, restore it
if ( normalizedState . from === expectedState ) {
lines [ lineIndex ] = line . replace (
2025-12-04 09:22:45 +00:00
normalizedState . to ,
2025-07-12 08:57:43 +00:00
normalizedState . from
) ;
restoredCount ++ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Restored line ${ lineIndex } : ${ normalizedState . to } → ${ normalizedState . from } for column " ${ columnName } " `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
}
}
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Restored ${ restoredCount } out of ${ normalizedStates . length } normalized states `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
return lines . join ( "\n" ) ;
2025-07-12 08:57:43 +00:00
}
/ * *
* NEW : Filter out normalizations that correspond to legitimate card movements
* /
private filterLegitimateNormalizations (
2025-12-04 09:22:45 +00:00
normalizedStates : Array < { line : number ; from : string ; to : string } > ,
knownMovements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
cardIndex : number ;
} > ,
2025-07-12 08:57:43 +00:00
newContent : string ,
kanbanFile : TFile
2025-12-04 09:22:45 +00:00
) : Array < { line : number ; from : string ; to : string } > {
const legitimateNormalizations : Array < {
line : number ;
from : string ;
to : string ;
} > = [ ] ;
const lines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
for ( const normalizedState of normalizedStates ) {
const lineIndex = normalizedState . line ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( lineIndex < lines . length ) {
const line = lines [ lineIndex ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Find which column this line is in
const columnName = this . findLineColumn ( line , lines , lineIndex ) ;
if ( columnName ) {
// Check if there's a known movement to this column
2025-12-04 09:22:45 +00:00
const hasMovementToColumn = knownMovements . some (
( movement ) = >
movement . newColumn . toLowerCase ( ) ===
columnName . toLowerCase ( )
2025-07-12 08:57:43 +00:00
) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( hasMovementToColumn ) {
// Get expected state for this column
2025-12-04 09:22:45 +00:00
const expectedState =
this . getCheckboxStateForColumn ( columnName ) ;
2025-07-12 08:57:43 +00:00
// If the normalization results in the expected state for this column, it's legitimate
if ( normalizedState . to === expectedState ) {
legitimateNormalizations . push ( normalizedState ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Legitimate normalization at line ${ lineIndex } : ${ normalizedState . from } → ${ normalizedState . to } for column " ${ columnName } " `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
}
}
}
return legitimateNormalizations ;
}
/ * *
* NEW : Check if card movements would cause unwanted normalization
* /
private checkMovementsForUnwantedNormalization (
2025-12-04 09:22:45 +00:00
cardMovements : Array < {
card : string ;
oldColumn : string ;
newColumn : string ;
} > ,
2025-07-12 08:57:43 +00:00
newContent : string ,
kanbanFile : TFile
) : boolean {
for ( const movement of cardMovements ) {
const { newColumn } = movement ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Get expected checkbox state for target column
const expectedState = this . getCheckboxStateForColumn ( newColumn ) ;
2025-12-04 09:22:45 +00:00
const expectedStateChar = expectedState . replace ( /[\[\]]/g , "" ) ;
2025-07-12 08:57:43 +00:00
// Check if the current content already has cards with correct states for this column
2025-12-04 09:22:45 +00:00
const lines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let inTargetColumn = false ;
2025-12-04 09:22:45 +00:00
let currentColumn = "" ;
2025-07-12 08:57:43 +00:00
for ( const line of lines ) {
2025-12-04 09:22:45 +00:00
if ( line . startsWith ( "## " ) ) {
2025-07-12 08:57:43 +00:00
currentColumn = line . substring ( 3 ) . trim ( ) ;
inTargetColumn = currentColumn === newColumn ;
continue ;
}
2025-12-04 09:22:45 +00:00
if ( inTargetColumn && line . trim ( ) . startsWith ( "- [" ) ) {
2025-07-12 08:57:43 +00:00
const match = line . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
if ( match ) {
const currentState = match [ 2 ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// If we find cards in target column that already have the expected state,
// and they would be changed to [x], this is unwanted normalization
2025-12-04 09:22:45 +00:00
if (
currentState === expectedStateChar &&
expectedStateChar !== "x"
) {
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Movement to column " ${ newColumn } " would cause unwanted normalization: [ ${ currentState } ] → [x] (expected: ${ expectedState } ) `
) ;
2025-07-12 08:57:43 +00:00
}
return true ;
}
}
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
return false ;
}
/ * *
* NEW : Find which column a line belongs to
* /
2025-12-04 09:22:45 +00:00
private findLineColumn (
line : string ,
allLines : string [ ] ,
lineIndex : number
) : string | null {
2025-07-12 08:57:43 +00:00
// Search backwards from current line to find the column header
for ( let i = lineIndex ; i >= 0 ; i -- ) {
const currentLine = allLines [ i ] ;
2025-12-04 09:22:45 +00:00
if ( currentLine . startsWith ( "## " ) ) {
2025-07-12 08:57:43 +00:00
return currentLine . substring ( 3 ) . trim ( ) ;
}
}
return null ;
}
/ * *
* NEW : Detect immediate Kanban normalization ( real - time detection )
* Enhanced to detect multiple - line normalization patterns
* /
2025-12-04 09:22:45 +00:00
private detectImmediateKanbanNormalization (
oldContent : string ,
newContent : string
) : boolean {
const oldLines = oldContent . split ( "\n" ) ;
const newLines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let normalizationCount = 0 ;
let customToXConversions = 0 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Look for lines where custom checkbox states were converted to [x]
for ( let i = 0 ; i < Math . min ( oldLines . length , newLines . length ) ; i ++ ) {
const oldLine = oldLines [ i ] ;
const newLine = newLines [ i ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
const oldMatch = oldLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const newMatch = newLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( oldMatch && newMatch ) {
const oldState = oldMatch [ 2 ] ;
const newState = newMatch [ 2 ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Count any checkbox state changes
if ( oldState !== newState ) {
normalizationCount ++ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Specifically track custom state → [x] conversions
2025-12-04 09:22:45 +00:00
if (
oldState !== " " &&
oldState !== "x" &&
newState === "x"
) {
2025-07-12 08:57:43 +00:00
customToXConversions ++ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Immediate normalization detected: [ ${ oldState } ] → [ ${ newState } ] on line ${ i } `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Enhanced detection criteria:
// 1. Multiple custom states converted to [x] (strong indicator of Kanban normalization)
// 2. High ratio of custom→[x] conversions vs total changes
const hasMultipleCustomToX = customToXConversions >= 2 ;
2025-12-04 09:22:45 +00:00
const hasHighCustomToXRatio =
normalizationCount > 0 &&
customToXConversions / normalizationCount >= 0.5 ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Enhanced normalization detection: ${ customToXConversions } custom→[x] out of ${ normalizationCount } total changes `
) ;
console . log (
` hasMultipleCustomToX: ${ hasMultipleCustomToX } , hasHighCustomToXRatio: ${ hasHighCustomToXRatio } `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
return hasMultipleCustomToX || hasHighCustomToXRatio ;
}
/ * *
* NEW : Revert Kanban normalization by restoring custom states
* Enhanced to handle multiple normalizations and better column detection
* /
2025-12-04 09:22:45 +00:00
private revertKanbanNormalization (
oldContent : string ,
newContent : string ,
kanbanFile : TFile
) : string {
const oldLines = oldContent . split ( "\n" ) ;
const newLines = newContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
const revertedLines = [ . . . newLines ] ;
let revertCount = 0 ;
let totalNormalizations = 0 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Find and revert unwanted normalizations
for ( let i = 0 ; i < Math . min ( oldLines . length , newLines . length ) ; i ++ ) {
const oldLine = oldLines [ i ] ;
const newLine = newLines [ i ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
const oldMatch = oldLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const newMatch = newLine . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( oldMatch && newMatch ) {
const oldState = oldMatch [ 2 ] ;
const newState = newMatch [ 2 ] ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Check if this is any normalization (not just custom → [x])
if ( oldState !== newState ) {
totalNormalizations ++ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Find which column this line is in
2025-12-04 09:22:45 +00:00
let currentColumn = "" ;
2025-07-12 08:57:43 +00:00
for ( let j = i ; j >= 0 ; j -- ) {
2025-12-04 09:22:45 +00:00
if ( newLines [ j ] . startsWith ( "## " ) ) {
// Use newLines to get current column structure
2025-07-12 08:57:43 +00:00
currentColumn = newLines [ j ] . substring ( 3 ) . trim ( ) ;
break ;
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( currentColumn ) {
// Get expected state for this column
2025-12-04 09:22:45 +00:00
const expectedState =
this . getCheckboxStateForColumn ( currentColumn ) ;
const expectedStateChar = expectedState . replace (
/[\[\]]/g ,
""
) ;
2025-07-12 08:57:43 +00:00
// Revert if:
// 1. Old state was correct for this column AND
// 2. New state is wrong for this column
2025-12-04 09:22:45 +00:00
if (
oldState === expectedStateChar &&
newState !== expectedStateChar
) {
2025-07-12 08:57:43 +00:00
// CRITICAL FIX: Only change the checkbox state, preserve the card content
const newPrefix = newMatch [ 1 ] ; // "- " part
2025-12-04 09:22:45 +00:00
const newSuffix = newLine . substring (
newMatch [ 0 ] . length
) ; // Everything after checkbox
revertedLines [
i
] = ` ${ newPrefix } [ ${ oldState } ] ${ newSuffix } ` ;
2025-07-12 08:57:43 +00:00
revertCount ++ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Reverted line ${ i } in column " ${ currentColumn } ": [ ${ newState } ] → [ ${ oldState } ] (preserving content) `
) ;
2025-07-12 08:57:43 +00:00
}
} else if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Line ${ i } in column " ${ currentColumn } ": [ ${ oldState } ] → [ ${ newState } ] (not reverting - expected: [ ${ expectedStateChar } ]) `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Reverted ${ revertCount } out of ${ totalNormalizations } normalizations `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
return revertedLines . join ( "\n" ) ;
2025-07-12 08:57:43 +00:00
}
/ * *
* Proactively sync all checkbox states in Kanban board to match column mappings
* This ensures all cards have the correct checkbox state for their column
* /
2025-12-04 09:22:45 +00:00
private async syncAllCheckboxStatesToMappings (
kanbanFile : TFile ,
content : string
) : Promise < string > {
2025-07-12 08:57:43 +00:00
try {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Syncing all checkbox states to mappings for: ${ kanbanFile . path } `
) ;
2025-07-12 08:57:43 +00:00
}
// Parse Kanban board structure
2025-12-04 09:22:45 +00:00
const kanban = await this . parseKanbanBoardContent (
content ,
kanbanFile
) ;
2025-07-12 08:57:43 +00:00
if ( ! kanban || Object . keys ( kanban ) . length === 0 ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Could not parse Kanban board structure, returning original content `
) ;
2025-07-12 08:57:43 +00:00
}
return content ;
}
// Split content into lines for position-based replacement
2025-12-04 09:22:45 +00:00
const lines = content . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let totalChanges = 0 ;
// Process each column and sync all cards to have correct checkbox states
for ( const [ columnName , columnData ] of Object . entries ( kanban ) ) {
2025-12-04 09:22:45 +00:00
const targetCheckboxState =
this . getCheckboxStateForColumn ( columnName ) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Syncing column " ${ columnName } " to checkbox state " ${ targetCheckboxState } " ( ${ columnData . items . length } cards) `
) ;
2025-07-12 08:57:43 +00:00
}
// Update each card in this column using position-based replacement
for ( const item of columnData . items ) {
// Check if the card already has the correct checkbox state
2025-12-04 09:22:45 +00:00
const currentCheckboxMatch =
item . text . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const currentCheckboxState = currentCheckboxMatch
? ` [ ${ currentCheckboxMatch [ 2 ] } ] `
: null ;
2025-07-12 08:57:43 +00:00
// Only update if the current state is different from target state
if ( currentCheckboxState !== targetCheckboxState ) {
2025-12-04 09:22:45 +00:00
const updatedCardText =
this . updateCheckboxStateInCardText (
item . text ,
targetCheckboxState
) ;
2025-07-12 08:57:43 +00:00
if ( updatedCardText !== item . text ) {
2025-12-04 09:22:45 +00:00
const cardPosition = this . findCardPositionInContent (
item . text ,
lines ,
columnName
) ;
2025-07-12 08:57:43 +00:00
if ( cardPosition !== - 1 ) {
// Replace only the specific card at the found position
2025-12-04 09:22:45 +00:00
const cardLines = item . text . split ( "\n" ) ;
const updatedCardLines =
updatedCardText . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
// Replace the card lines at the specific position
2025-12-04 09:22:45 +00:00
lines . splice (
cardPosition ,
cardLines . length ,
. . . updatedCardLines
) ;
2025-07-12 08:57:43 +00:00
totalChanges ++ ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Synced card at position ${ cardPosition } : ${ item . text . substring (
0 ,
30
) } . . . → $ { targetCheckboxState } ( was $ { currentCheckboxState } ) `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
} else {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Skipping card (already has correct state ${ targetCheckboxState } ): ${ item . text . substring (
0 ,
30
) } . . . `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
}
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Sync complete: ${ totalChanges } cards updated to match column mappings `
) ;
2025-07-12 08:57:43 +00:00
}
2025-12-04 09:22:45 +00:00
return lines . join ( "\n" ) ;
2025-03-16 19:20:48 +00:00
} catch ( error ) {
2025-07-12 08:57:43 +00:00
console . error ( "Error syncing checkbox states to mappings:" , error ) ;
if ( this . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
return content ; // Return original content if there's an error
2025-03-16 19:20:48 +00:00
}
}
2025-07-12 08:57:43 +00:00
/ * *
* Auto - sync all checkbox states in a Kanban board to match column mappings
* Runs once per file per session for performance optimization
* Uses position - based replacement to avoid conflicts
* /
2025-12-04 09:22:45 +00:00
private async autoSyncKanbanCheckboxStates (
kanbanFile : TFile
) : Promise < void > {
2025-03-16 19:20:48 +00:00
try {
2025-07-12 08:57:43 +00:00
// Check if we're already updating to prevent conflicts
if ( this . isUpdatingFromKanban ) {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Auto-sync skipped - update already in progress for: ${ kanbanFile . path } `
) ;
2025-07-12 08:57:43 +00:00
}
return ;
2025-03-16 19:20:48 +00:00
}
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Starting auto-sync for Kanban board: ${ kanbanFile . path } `
) ;
2025-07-12 08:57:43 +00:00
}
2025-03-19 11:33:52 +00:00
2025-07-12 08:57:43 +00:00
// Set flag to prevent conflicts
this . isUpdatingFromKanban = true ;
// Mark this file as auto-synced to prevent repeat runs
this . autoSyncedFiles . add ( kanbanFile . path ) ;
// Update timestamp to track when auto-sync ran
this . lastFileUpdateMap . set ( kanbanFile . path , Date . now ( ) ) ;
// Store initial content for comparison
const initialContent = await this . app . vault . read ( kanbanFile ) ;
this . lastKanbanContent . set ( kanbanFile . path , initialContent ) ;
// Read current file content
const content = await this . app . vault . read ( kanbanFile ) ;
// Parse Kanban board structure
2025-12-04 09:22:45 +00:00
const kanban = await this . parseKanbanBoardContent (
content ,
kanbanFile
) ;
2025-07-12 08:57:43 +00:00
// Split content into lines for position-based replacement
2025-12-04 09:22:45 +00:00
const lines = content . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
let totalChanges = 0 ;
// Process each column and update all cards to have correct checkbox states
for ( const [ columnName , columnData ] of Object . entries ( kanban ) ) {
2025-12-04 09:22:45 +00:00
const targetCheckboxState =
this . getCheckboxStateForColumn ( columnName ) ;
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Auto-syncing column " ${ columnName } " to checkbox state " ${ targetCheckboxState } " ( ${ columnData . items . length } cards) `
) ;
2025-07-12 08:57:43 +00:00
}
// Update each card in this column using position-based replacement
for ( const item of columnData . items ) {
// Check if the card already has the correct checkbox state
2025-12-04 09:22:45 +00:00
const currentCheckboxMatch =
item . text . match ( /^(\s*- )\[([^\]]*)\]/ ) ;
const currentCheckboxState = currentCheckboxMatch
? ` [ ${ currentCheckboxMatch [ 2 ] } ] `
: null ;
2025-07-12 08:57:43 +00:00
// Only update if the current state is different from target state
if ( currentCheckboxState !== targetCheckboxState ) {
2025-12-04 09:22:45 +00:00
const updatedCardText =
this . updateCheckboxStateInCardText (
item . text ,
targetCheckboxState
) ;
2025-07-12 08:57:43 +00:00
if ( updatedCardText !== item . text ) {
2025-12-04 09:22:45 +00:00
const cardPosition = this . findCardPositionInContent (
item . text ,
lines ,
columnName
) ;
2025-07-12 08:57:43 +00:00
if ( cardPosition !== - 1 ) {
// Replace only the specific card at the found position
2025-12-04 09:22:45 +00:00
const cardLines = item . text . split ( "\n" ) ;
const updatedCardLines =
updatedCardText . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
// Replace the card lines at the specific position
2025-12-04 09:22:45 +00:00
lines . splice (
cardPosition ,
cardLines . length ,
. . . updatedCardLines
) ;
2025-07-12 08:57:43 +00:00
totalChanges ++ ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Updated card at position ${ cardPosition } : ${ item . text . substring (
0 ,
30
) } . . . → $ { targetCheckboxState } ( was $ { currentCheckboxState } ) `
) ;
2025-07-12 08:57:43 +00:00
}
}
}
} else {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Skipping card (already has correct state ${ targetCheckboxState } ): ${ item . text . substring (
0 ,
30
) } . . . `
) ;
2025-07-12 08:57:43 +00:00
}
2025-04-17 05:02:34 +00:00
}
2025-07-12 08:57:43 +00:00
}
2025-04-17 05:02:34 +00:00
}
2025-07-12 08:57:43 +00:00
// Update the file if changes were made
if ( totalChanges > 0 ) {
2025-12-04 09:22:45 +00:00
const updatedContent = lines . join ( "\n" ) ;
2025-07-12 08:57:43 +00:00
await this . app . vault . modify ( kanbanFile , updatedContent ) ;
// CRITICAL: Update lastKanbanContent to prevent conflicts with card movement detection
this . lastKanbanContent . set ( kanbanFile . path , updatedContent ) ;
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Auto-sync complete: ${ totalChanges } cards updated in ${ kanbanFile . basename } `
) ;
console . log (
` Updated lastKanbanContent for ${ kanbanFile . path } to prevent conflicts `
) ;
2025-07-12 08:57:43 +00:00
}
// Show notification to user
2025-12-04 09:22:45 +00:00
new Notice (
` Auto-synced ${ totalChanges } card checkbox states in ${ kanbanFile . basename } `
) ;
2025-07-12 08:57:43 +00:00
} else {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Auto-sync complete: No changes needed for ${ kanbanFile . basename } `
) ;
2025-07-12 08:57:43 +00:00
}
2025-03-16 19:20:48 +00:00
}
} catch ( error ) {
2025-07-12 08:57:43 +00:00
console . error ( "Error in auto-sync Kanban checkbox states:" , error ) ;
if ( this . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
// Remove from auto-synced set so it can be retried
this . autoSyncedFiles . delete ( kanbanFile . path ) ;
} finally {
// Always reset flag after operation completes
setTimeout ( ( ) = > {
this . isUpdatingFromKanban = false ;
if ( this . settings . showDebugInfo ) {
console . log ( ` Auto-sync flag reset for: ${ kanbanFile . path } ` ) ;
}
} , 200 ) ; // Slightly longer delay to ensure all operations complete
2025-03-16 19:20:48 +00:00
}
2022-04-15 18:13:31 +00:00
}
2025-05-16 05:21:07 +00:00
/ * *
2025-07-12 08:57:43 +00:00
* Force refresh Kanban UI after modifying file content
* Tries multiple approaches to ensure UI reflects the updated checkbox states
2025-05-16 05:21:07 +00:00
* /
2025-07-12 08:57:43 +00:00
private async forceRefreshKanbanUI ( kanbanFile : TFile ) : Promise < void > {
try {
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Attempting to force refresh Kanban UI for: ${ kanbanFile . path } `
) ;
2025-07-12 08:57:43 +00:00
}
2025-05-16 05:21:07 +00:00
2025-07-12 08:57:43 +00:00
// Method 1: Skip MetadataCache trigger to avoid conflicts
// this.app.metadataCache.trigger("changed", kanbanFile); // Can cause errors
2025-05-16 05:21:07 +00:00
2025-07-12 08:57:43 +00:00
// Method 2: If this is the currently active file, try to refresh the view
const activeFile = this . app . workspace . getActiveFile ( ) ;
if ( activeFile && activeFile . path === kanbanFile . path ) {
// Try to refresh the active view
2025-12-04 09:22:45 +00:00
const activeView =
this . app . workspace . getActiveViewOfType ( MarkdownView ) ;
2025-07-12 08:57:43 +00:00
if ( activeView ) {
// Force re-render by triggering a view update
setTimeout ( ( ) = > {
activeView . requestSave ( ) ;
} , 100 ) ;
}
2025-05-16 05:21:07 +00:00
}
2025-07-12 08:57:43 +00:00
// Method 3: Trigger workspace layout change to force refresh
setTimeout ( ( ) = > {
this . app . workspace . trigger ( "layout-change" ) ;
} , 150 ) ;
// Method 4: If file is open in a leaf, try to refresh that leaf
const leaves = this . app . workspace . getLeavesOfType ( "markdown" ) ;
for ( const leaf of leaves ) {
const view = leaf . view as MarkdownView ;
if ( view . file && view . file . path === kanbanFile . path ) {
setTimeout ( ( ) = > {
// Force view to re-read file content
view . load ( ) ;
} , 200 ) ;
break ;
2025-05-16 05:21:07 +00:00
}
}
2025-07-12 08:57:43 +00:00
if ( this . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Force refresh attempts completed for: ${ kanbanFile . path } `
) ;
2025-07-12 08:57:43 +00:00
}
} catch ( error ) {
if ( this . settings . showDebugInfo ) {
console . error ( "Error forcing Kanban UI refresh:" , error ) ;
}
2025-05-16 05:21:07 +00:00
}
}
2022-04-15 18:13:31 +00:00
}
2025-03-16 09:26:27 +00:00
class TaskProgressBarView extends ItemView {
plugin : TaskProgressBarPlugin ;
currentFile : TFile | null = null ;
isVisible : boolean = false ;
lastUpdateTime : number = 0 ;
2025-03-16 19:20:48 +00:00
lastFileUpdateMap : Map < string , number > = new Map ( ) ; // Track last update time per file
initialLoadComplete : boolean = false ; // Track if initial load is complete
2025-03-19 11:33:52 +00:00
completedFilesMap : Map < string , boolean > = new Map ( ) ; // Track which files have been marked as completed
2025-03-16 09:26:27 +00:00
constructor ( leaf : WorkspaceLeaf , plugin : TaskProgressBarPlugin ) {
super ( leaf ) ;
this . plugin = plugin ;
2022-04-15 18:13:31 +00:00
}
2025-03-16 09:26:27 +00:00
getViewType ( ) : string {
2025-03-16 19:20:48 +00:00
return "progress-tracker" ;
2022-04-15 18:13:31 +00:00
}
2025-03-16 09:26:27 +00:00
getDisplayText ( ) : string {
2025-04-15 10:53:27 +00:00
return "Task progress bar" ;
2025-03-16 09:26:27 +00:00
}
getIcon ( ) : string {
2025-03-16 19:20:48 +00:00
return "bar-chart-horizontal" ;
2025-03-16 09:26:27 +00:00
}
async onOpen() {
this . isVisible = true ;
2025-03-16 19:20:48 +00:00
this . initialLoadComplete = false ;
2025-04-17 05:02:34 +00:00
2025-12-04 09:22:45 +00:00
// Add custom class to parent workspace-leaf (using multiple methods for reliability)
2025-04-17 05:02:34 +00:00
const leaf = this . leaf as any ;
if ( leaf && leaf . containerEl ) {
leaf . containerEl . addClass ( "progress-tracker-leaf" ) ;
}
2025-12-04 09:22:45 +00:00
// Also try to add class to parent element directly
const parentLeaf = this . containerEl . closest ( ".workspace-leaf" ) ;
if ( parentLeaf ) {
parentLeaf . addClass ( "progress-tracker-leaf" ) ;
}
2025-03-16 09:26:27 +00:00
const container = this . containerEl . children [ 1 ] ;
container . empty ( ) ;
2025-03-16 19:20:48 +00:00
// Create progress container
const progressContainer = container . createDiv ( {
cls : "task-progress-container" ,
} ) ;
// Add loading indicator that we'll keep until the first real data loads
progressContainer . createEl ( "p" , {
text : "Loading task progress..." ,
cls : "loading-indicator" ,
} ) ;
2025-12-04 09:22:45 +00:00
// Trigger initial update after a short delay to ensure workspace is ready
setTimeout ( async ( ) = > {
const currentFile = this . plugin . app . workspace . getActiveFile ( ) ;
if ( currentFile ) {
await this . updateProgressBar ( currentFile , undefined , true ) ;
}
} , 200 ) ;
2025-03-16 09:26:27 +00:00
}
2025-03-16 19:20:48 +00:00
async updateProgressBar (
file : TFile | null ,
content? : string ,
isInitialLoad : boolean = false
) {
2025-03-16 09:26:27 +00:00
if ( ! file ) return ;
2025-03-16 19:20:48 +00:00
// Track the update time for this file
this . lastFileUpdateMap . set ( file . path , Date . now ( ) ) ;
2025-03-20 11:32:16 +00:00
// Avoid updating too quickly
2025-03-16 09:26:27 +00:00
const now = Date . now ( ) ;
2025-03-16 19:20:48 +00:00
if ( ! isInitialLoad && now - this . lastUpdateTime < 100 ) return ;
2025-03-16 09:26:27 +00:00
this . lastUpdateTime = now ;
2025-03-16 19:20:48 +00:00
2025-03-16 09:26:27 +00:00
this . currentFile = file ;
2025-03-16 19:20:48 +00:00
2025-03-16 09:26:27 +00:00
const container = this . containerEl . children [ 1 ] ;
2025-03-16 19:20:48 +00:00
const progressContainer = container . querySelector (
".task-progress-container"
) as HTMLElement ;
2025-03-16 09:26:27 +00:00
if ( ! progressContainer ) return ;
2025-03-16 19:20:48 +00:00
// Clear the container once at the start if this is initial load
// This prevents showing the loading indicator and then progress bar
if ( isInitialLoad || ! this . initialLoadComplete ) {
progressContainer . empty ( ) ;
this . initialLoadComplete = true ;
}
2025-03-20 11:32:16 +00:00
// Add class to show animation only when showUpdateAnimation setting is enabled
2025-03-16 18:17:32 +00:00
if ( this . plugin . settings . showUpdateAnimation ) {
2025-03-16 19:20:48 +00:00
progressContainer . classList . add ( "updating" ) ;
2025-03-16 18:17:32 +00:00
}
2025-03-16 19:20:48 +00:00
try {
2025-03-20 11:32:16 +00:00
// Update immediately if content is provided
2025-03-16 19:20:48 +00:00
if ( content ) {
2025-07-12 08:57:43 +00:00
if ( ! this . hasTasksInContentExtended ( content ) ) {
2025-03-20 10:59:28 +00:00
// Only clear if not already showing "no tasks" message
if ( ! progressContainer . querySelector ( ".no-tasks-message" ) ) {
progressContainer . empty ( ) ;
progressContainer . createEl ( "p" , {
text : "No tasks found in this file" ,
2025-04-15 11:25:00 +00:00
cls : "no-tasks-message" ,
2025-03-20 10:59:28 +00:00
} ) ;
}
2025-03-17 10:53:48 +00:00
} else {
2025-03-20 10:59:28 +00:00
// Create/update progress bar with content
2025-03-19 11:33:52 +00:00
this . createProgressBarFromString (
2025-03-17 10:53:48 +00:00
progressContainer ,
2025-03-19 11:33:52 +00:00
content ,
2025-03-17 10:53:48 +00:00
file
) ;
}
2025-03-16 19:20:48 +00:00
} else {
2025-03-20 11:32:16 +00:00
// Read file content
2025-03-16 19:20:48 +00:00
const fileContent = await this . plugin . app . vault . read ( file ) ;
2025-07-12 08:57:43 +00:00
if ( ! this . hasTasksInContentExtended ( fileContent ) ) {
2025-03-20 10:59:28 +00:00
// Only clear if not already showing "no tasks" message
if ( ! progressContainer . querySelector ( ".no-tasks-message" ) ) {
progressContainer . empty ( ) ;
progressContainer . createEl ( "p" , {
text : "No tasks found in this file" ,
2025-04-15 11:25:00 +00:00
cls : "no-tasks-message" ,
2025-03-20 10:59:28 +00:00
} ) ;
}
2025-03-17 10:53:48 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log ( "No tasks found in file:" , file . path ) ;
}
} else {
2025-03-20 10:59:28 +00:00
// Create/update progress bar with content
2025-03-19 11:33:52 +00:00
this . createProgressBarFromString (
2025-03-16 19:20:48 +00:00
progressContainer ,
2025-03-19 11:33:52 +00:00
fileContent ,
2025-03-16 19:20:48 +00:00
file
) ;
}
}
} catch ( error ) {
console . error ( "Error updating progress bar:" , error ) ;
2025-03-17 10:53:48 +00:00
progressContainer . empty ( ) ;
progressContainer . createEl ( "p" , {
2025-03-19 11:33:52 +00:00
text : ` Error updating progress bar: ${ error . message } ` ,
2025-03-17 10:53:48 +00:00
} ) ;
2025-03-16 19:20:48 +00:00
} finally {
2025-03-20 11:32:16 +00:00
// Remove class after update completes, only if animation is enabled
2025-03-16 18:17:32 +00:00
if ( this . plugin . settings . showUpdateAnimation ) {
setTimeout ( ( ) = > {
2025-03-16 19:20:48 +00:00
progressContainer . classList . remove ( "updating" ) ;
2025-03-16 18:17:32 +00:00
} , this . plugin . settings . updateAnimationDelay ) ; // Use configurable delay
}
2025-03-16 09:26:27 +00:00
}
}
2025-03-16 19:20:48 +00:00
// Helper method to quickly check if content has tasks
2025-07-12 08:57:43 +00:00
hasTasksInContentExtended ( content : string ) : boolean {
// Extended pattern to match custom checkbox states
const extendedTaskRegex = /- \[[^\]]*\]/i ;
return extendedTaskRegex . test ( content ) ;
2025-03-16 09:26:27 +00:00
}
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// Method to create progress bar from string content
2025-03-16 19:20:48 +00:00
async createProgressBarFromString (
container : HTMLElement ,
content : string ,
file : TFile
) {
2025-03-16 09:26:27 +00:00
try {
2025-03-16 19:20:48 +00:00
// Log for debugging if enabled
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Creating progress bar for file: ${ file . path } ` ) ;
console . log ( ` Content length: ${ content . length } ` ) ;
}
2025-03-19 11:33:52 +00:00
// Get Dataview API - only check for warning display
const dvAPI = this . plugin . dvAPI ;
if ( ! dvAPI ) {
2025-03-20 10:59:28 +00:00
// Only clear and create warning if not already showing dataview warning
if ( ! container . querySelector ( ".dataview-warning-compact" ) ) {
container . empty ( ) ;
const dataviewWarning = container . createDiv ( {
cls : "dataview-warning-compact" ,
} ) ;
dataviewWarning . createEl ( "span" , {
text : "Dataview not available" ,
cls : "dataview-warning-text" ,
} ) ;
}
2025-03-19 11:33:52 +00:00
return ;
}
2025-07-12 08:57:43 +00:00
// Count tasks with support for custom checkbox states
let incompleteTasks = 0 ;
let completedTasks = 0 ;
let customStateTasks = 0 ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . plugin . settings . enableCustomCheckboxStates ) {
// When custom checkbox states are enabled, use enhanced counting
const taskCounts = this . countTasksByCheckboxState ( content ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Count standard states
2025-12-04 09:22:45 +00:00
incompleteTasks = taskCounts [ " " ] || 0 ; // [ ]
completedTasks = taskCounts [ "x" ] || 0 ; // [x]
2025-07-12 08:57:43 +00:00
// Count all custom states as tasks in progress
for ( const [ state , count ] of Object . entries ( taskCounts ) ) {
2025-12-04 09:22:45 +00:00
if ( state !== " " && state !== "x" && state . trim ( ) !== "" ) {
2025-07-12 08:57:43 +00:00
customStateTasks += count ;
}
}
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log ( "Custom checkbox state counts:" , taskCounts ) ;
console . log (
` Incomplete: ${ incompleteTasks } , Completed: ${ completedTasks } , Custom states: ${ customStateTasks } `
) ;
2025-07-12 08:57:43 +00:00
}
} else {
// Legacy counting - only [ ] and [x]
incompleteTasks = ( content . match ( /- \[ \]/g ) || [ ] ) . length ;
completedTasks = ( content . match ( /- \[x\]/gi ) || [ ] ) . length ;
}
2025-12-04 09:22:45 +00:00
let totalTasks =
incompleteTasks + completedTasks + customStateTasks ;
2025-03-16 19:20:48 +00:00
2025-07-12 08:57:43 +00:00
// Try with relaxed regex if needed (for legacy compatibility)
2025-03-16 19:20:48 +00:00
let relaxedIncompleteTasks = 0 ;
let relaxedCompletedTasks = 0 ;
2025-03-16 09:26:27 +00:00
if ( totalTasks === 0 ) {
2025-04-15 11:25:00 +00:00
relaxedIncompleteTasks = ( content . match ( /[-*] \[ \]/g ) || [ ] )
. length ;
relaxedCompletedTasks = ( content . match ( /[-*] \[x\]/gi ) || [ ] )
. length ;
2025-03-16 19:20:48 +00:00
totalTasks = relaxedIncompleteTasks + relaxedCompletedTasks ;
2025-03-16 09:26:27 +00:00
}
2025-03-16 19:20:48 +00:00
// Log task counts for debugging
2025-03-16 09:26:27 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-03-16 19:20:48 +00:00
console . log (
2025-07-12 08:57:43 +00:00
` Task counts - incomplete: ${ incompleteTasks } , completed: ${ completedTasks } , custom states: ${ customStateTasks } , total: ${ totalTasks } `
2025-03-16 19:20:48 +00:00
) ;
if ( relaxedIncompleteTasks > 0 || relaxedCompletedTasks > 0 ) {
console . log (
` Using relaxed regex - found tasks: ${
relaxedIncompleteTasks + relaxedCompletedTasks
} `
) ;
}
}
if ( totalTasks === 0 ) {
2025-03-20 10:59:28 +00:00
// No tasks found, show message - but only if not already showing the message
if ( ! container . querySelector ( ".no-tasks-message" ) ) {
container . empty ( ) ;
container . createEl ( "p" , {
text : "No tasks found in this file" ,
2025-04-15 11:25:00 +00:00
cls : "no-tasks-message" ,
2025-03-20 10:59:28 +00:00
} ) ;
}
2025-03-16 19:20:48 +00:00
return ;
2025-03-16 09:26:27 +00:00
}
2025-03-16 19:20:48 +00:00
// Calculate percentage based on which regex found tasks
let completedCount =
2025-12-04 09:22:45 +00:00
incompleteTasks > 0 ||
completedTasks > 0 ||
customStateTasks > 0
2025-03-16 19:20:48 +00:00
? completedTasks
: relaxedCompletedTasks ;
const percentage = Math . round ( ( completedCount / totalTasks ) * 100 ) ;
2025-05-16 05:01:17 +00:00
// Update UI first for better responsiveness
2025-12-04 09:22:45 +00:00
this . updateProgressBarUI (
container ,
percentage ,
completedCount ,
totalTasks
) ;
2025-03-20 07:21:00 +00:00
2025-05-16 05:01:17 +00:00
// Then process status and Kanban updates asynchronously
2025-12-04 09:22:45 +00:00
this . processStatusAndKanbanUpdates (
file ,
percentage ,
completedCount ,
totalTasks
) ;
2025-05-16 05:01:17 +00:00
} catch ( error ) {
console . error ( "Error creating progress bar from string:" , error ) ;
container . empty ( ) ;
container . createEl ( "p" , {
text : ` Error creating progress bar: ${ error . message } ` ,
} ) ;
}
}
2025-03-20 09:43:08 +00:00
2025-05-16 05:01:17 +00:00
/ * *
2025-05-16 05:31:46 +00:00
* Update UI elements first for better responsiveness , then process status and Kanban updates asynchronously
2025-05-16 05:01:17 +00:00
* /
private updateProgressBarUI (
container : HTMLElement ,
percentage : number ,
completedCount : number ,
totalTasks : number
) {
// Check if we already have progress elements
let progressLayout = container . querySelector (
".progress-layout"
) as HTMLElement ;
let statsContainer = container . querySelector (
".progress-stats-compact"
) as HTMLElement ;
2025-03-19 11:33:52 +00:00
2025-05-16 05:01:17 +00:00
// If no existing elements, create container structure
if ( ! progressLayout || ! statsContainer ) {
container . empty ( ) ;
2025-03-24 09:40:04 +00:00
2025-05-16 05:01:17 +00:00
// Create a more compact layout
progressLayout = container . createDiv ( {
cls : "progress-layout" ,
} ) ;
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Create percentage text element
progressLayout . createEl ( "div" , {
cls : "progress-percentage-small" ,
} ) ;
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Create HTML5-like progress bar
const progressBarContainer = progressLayout . createDiv ( {
cls : "pt-progress-bar-container" ,
} ) ;
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Create the outer progress element
const progressElement = progressBarContainer . createDiv ( {
cls : "progress-element" ,
} ) ;
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Create the inner value element that will be animated
progressElement . createDiv ( {
cls : "progress-value" ,
} ) ;
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Create stats container at the bottom
statsContainer = container . createDiv ( {
cls : "progress-stats-compact" ,
} ) ;
}
// Update percentage text
const percentageElement = progressLayout . querySelector (
".progress-percentage-small"
) as HTMLElement ;
if ( percentageElement ) {
percentageElement . setText ( ` ${ percentage } % ` ) ;
}
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Update progress bar width with smooth transition
const progressValue = container . querySelector (
".progress-value"
) as HTMLElement ;
if ( progressValue ) {
// Add transition style if not already present
if ( ! progressValue . hasAttribute ( "data-has-transition" ) ) {
progressValue . style . transition =
"width 0.3s ease-in-out, background-color 0.3s ease" ;
progressValue . setAttribute ( "data-has-transition" , "true" ) ;
2025-03-20 10:59:28 +00:00
}
2025-05-16 05:01:17 +00:00
progressValue . style . width = ` ${ percentage } % ` ;
this . applyProgressColor ( progressValue , percentage ) ;
}
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Update progress element data attribute
const progressElement = container . querySelector (
".progress-element"
) as HTMLElement ;
if ( progressElement ) {
progressElement . setAttribute (
"data-percentage" ,
percentage . toString ( )
) ;
}
// Update stats text
if ( statsContainer ) {
statsContainer . empty ( ) ;
statsContainer . createSpan ( {
text : ` ${ completedCount } / ${ totalTasks } tasks ` ,
} ) ;
}
2025-04-15 11:25:00 +00:00
2025-05-16 05:01:17 +00:00
// Update debug info if needed
if ( this . plugin . settings . showDebugInfo ) {
let debugInfo = container . querySelector (
".debug-info"
) as HTMLElement ;
if ( ! debugInfo ) {
debugInfo = container . createDiv ( { cls : "debug-info" } ) ;
2025-03-20 10:59:28 +00:00
} else {
2025-05-16 05:01:17 +00:00
debugInfo . empty ( ) ;
2025-03-20 10:59:28 +00:00
}
2025-05-16 05:01:17 +00:00
debugInfo . createEl ( "p" , { text : ` Debug info: ` } ) ;
debugInfo . createEl ( "p" , {
text : ` File: ${ this . currentFile ? . path } ` ,
} ) ;
debugInfo . createEl ( "p" , {
text : ` Incomplete tasks: ${ totalTasks - completedCount } ` ,
} ) ;
debugInfo . createEl ( "p" , {
text : ` Completed tasks: ${ completedCount } ` ,
2025-03-17 10:53:48 +00:00
} ) ;
2025-05-16 05:01:17 +00:00
debugInfo . createEl ( "p" , { text : ` Total tasks: ${ totalTasks } ` } ) ;
debugInfo . createEl ( "p" , { text : ` Percentage: ${ percentage } % ` } ) ;
debugInfo . createEl ( "p" , {
text : ` Update time: ${ new Date ( ) . toISOString ( ) } ` ,
} ) ;
debugInfo . createEl ( "p" , {
text : ` Color scheme: ${ this . plugin . settings . progressColorScheme } ` ,
} ) ;
} else {
// Remove debug info if it exists but debug is disabled
const debugInfo = container . querySelector ( ".debug-info" ) ;
if ( debugInfo ) debugInfo . remove ( ) ;
2025-03-16 19:20:48 +00:00
}
}
2025-05-16 05:01:17 +00:00
/ * *
* Process status and Kanban updates asynchronously
* /
private async processStatusAndKanbanUpdates (
file : TFile ,
percentage : number ,
completedCount : number ,
totalTasks : number
) {
// Use setTimeout to process these updates in the next tick
setTimeout ( async ( ) = > {
try {
// Update status based on progress percentage
let statusChanged = false ;
if ( this . plugin . settings . autoChangeStatus ) {
statusChanged = await this . updateStatusBasedOnProgress (
file ,
percentage
) ;
}
// Update Kanban boards based on task progress
if (
this . plugin . settings . autoUpdateKanban &&
( statusChanged || ! this . completedFilesMap . has ( file . path ) )
) {
2025-12-04 09:22:45 +00:00
await this . updateKanbanBoards (
file ,
completedCount ,
totalTasks
) ;
2025-05-16 05:01:17 +00:00
}
// Additional special handling for 100% completion
2025-12-04 09:22:45 +00:00
if (
percentage === 100 &&
this . plugin . settings . autoUpdateMetadata
) {
2025-05-16 05:01:17 +00:00
// Only update metadata and show notification if this file hasn't been marked as completed yet
if ( ! this . completedFilesMap . has ( file . path ) ) {
await this . updateFileMetadata ( file ) ;
// Mark this file as completed to avoid repeated updates
this . completedFilesMap . set ( file . path , true ) ;
}
} else if ( percentage < 100 ) {
// If percentage is less than 100%, remove from completed files map
if ( this . completedFilesMap . has ( file . path ) ) {
this . completedFilesMap . delete ( file . path ) ;
}
}
// Auto-add to Kanban board if enabled and file has tasks
if (
this . plugin . settings . autoAddToKanban &&
this . plugin . settings . autoAddKanbanBoard &&
totalTasks > 0 &&
! this . completedFilesMap . has ( file . path )
) {
await this . addFileToKanbanBoard ( file ) ;
}
} catch ( error ) {
console . error ( "Error in status and Kanban updates:" , error ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
}
} , 0 ) ;
}
2025-03-20 10:59:28 +00:00
// Method to apply color based on percentage and settings - keep logic but add smooth transition
applyProgressColor ( progressElement : HTMLElement , percentage : number ) {
const settings = this . plugin . settings ;
2025-03-20 09:43:08 +00:00
2025-03-20 10:59:28 +00:00
// If using default color scheme, let CSS handle it
if ( settings . progressColorScheme === "default" ) {
2025-03-20 11:32:16 +00:00
// Remove any previously set inline colors
2025-03-20 10:59:28 +00:00
progressElement . style . backgroundColor = "" ;
return ;
}
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// Apply custom color based on percentage
let newColor = "" ;
if ( percentage === 100 ) {
2025-03-20 11:32:16 +00:00
// Complete - green
2025-03-20 10:59:28 +00:00
newColor = settings . completeProgressColor ;
} else if ( percentage >= settings . mediumProgressThreshold ) {
2025-03-20 11:32:16 +00:00
// High progress (66-99%) - blue
2025-03-20 10:59:28 +00:00
newColor = settings . highProgressColor ;
} else if ( percentage >= settings . lowProgressThreshold ) {
2025-03-20 11:32:16 +00:00
// Medium progress (34-65%) - orange/yellow
2025-03-20 10:59:28 +00:00
newColor = settings . mediumProgressColor ;
} else {
2025-03-20 11:32:16 +00:00
// Low progress (0-33%) - red
2025-03-20 10:59:28 +00:00
newColor = settings . lowProgressColor ;
}
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// Only update if color has changed
if ( progressElement . style . backgroundColor !== newColor ) {
progressElement . style . backgroundColor = newColor ;
}
2025-03-20 09:43:08 +00:00
2025-03-20 11:32:16 +00:00
// Add debug log if needed
2025-03-20 10:59:28 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Applied color for ${ percentage } %:
Color scheme : $ { settings . progressColorScheme } ,
Low threshold : $ { settings . lowProgressThreshold } % ,
Medium threshold : $ { settings . mediumProgressThreshold } % ,
High threshold : $ { settings . highProgressThreshold } % ,
Applied color : $ { newColor } ` );
2025-03-20 07:21:00 +00:00
}
}
2025-03-20 09:43:08 +00:00
/ * *
2025-03-20 10:59:28 +00:00
* Clear the completed files cache
* Used to reset notifications for completed files
2025-03-20 09:43:08 +00:00
* /
2025-03-20 10:59:28 +00:00
clearCompletedFilesCache() {
this . completedFilesMap . clear ( ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . log ( "Cleared completed files cache" ) ;
2025-03-20 09:43:08 +00:00
}
}
2025-03-20 10:59:28 +00:00
async updateStatusBasedOnProgress (
2025-04-15 11:25:00 +00:00
file : TFile ,
2025-03-20 10:59:28 +00:00
progressPercentage : number
) : Promise < boolean > {
if ( ! file || ! this . plugin . settings . autoChangeStatus ) return false ;
2025-03-20 07:21:00 +00:00
2025-03-19 11:33:52 +00:00
try {
2025-04-15 11:25:00 +00:00
let needsUpdate = false ;
2025-03-19 11:33:52 +00:00
2025-04-15 11:25:00 +00:00
// Determine target status based on progress percentage
let targetStatus = this . plugin . settings . statusInProgress ;
if ( progressPercentage === 0 ) {
targetStatus = this . plugin . settings . statusTodo ;
} else if ( progressPercentage === 100 ) {
targetStatus = this . plugin . settings . statusCompleted ;
2025-03-19 11:33:52 +00:00
}
2025-04-15 11:25:00 +00:00
// Use processFrontMatter API to update frontmatter
await this . app . fileManager . processFrontMatter (
file ,
( frontmatter ) = > {
// Check current status
const currentStatus = frontmatter [ "status" ] ;
2025-03-19 12:25:02 +00:00
2025-04-15 11:25:00 +00:00
// Update if status is different
if ( currentStatus !== targetStatus ) {
frontmatter [ "status" ] = targetStatus ;
2025-03-19 12:25:02 +00:00
needsUpdate = true ;
}
2025-03-19 11:33:52 +00:00
2025-04-15 11:25:00 +00:00
// Remove finished date if progress is less than 100%
if (
progressPercentage < 100 &&
this . plugin . settings . autoUpdateFinishedDate
) {
if ( frontmatter [ "finished" ] ) {
delete frontmatter [ "finished" ] ;
needsUpdate = true ;
}
2025-03-19 11:33:52 +00:00
}
}
2025-04-15 11:25:00 +00:00
) ;
if ( needsUpdate && this . plugin . settings . showDebugInfo ) {
console . log (
` Updated status to " ${ targetStatus } " based on progress ${ progressPercentage } % for file: ` ,
file . path
) ;
2025-03-19 11:33:52 +00:00
}
2025-04-15 11:25:00 +00:00
return needsUpdate ;
} catch ( error ) {
console . error ( "Error updating status based on progress:" , error ) ;
return false ;
}
}
2025-03-19 11:33:52 +00:00
2025-04-15 11:25:00 +00:00
// New method to update file metadata when tasks are completed
async updateFileMetadata ( file : TFile ) {
try {
// use processFrontMatter API to update metadata
2025-04-15 13:44:37 +00:00
await this . app . fileManager . processFrontMatter (
file ,
( frontmatter ) = > {
let needsUpdate = false ;
const today = new Date ( ) . toISOString ( ) . split ( "T" ) [ 0 ] ; // Format: YYYY-MM-DD
// update status if it enabled
if ( this . plugin . settings . autoChangeStatus ) {
const targetStatus =
this . plugin . settings . statusCompleted ;
if ( frontmatter [ "status" ] !== targetStatus ) {
frontmatter [ "status" ] = targetStatus ;
needsUpdate = true ;
2025-03-19 11:33:52 +00:00
2025-04-15 13:44:37 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Updating status to ${ targetStatus } in file: ` ,
file . path
) ;
}
2025-04-15 11:25:00 +00:00
}
}
2025-03-19 11:33:52 +00:00
2025-04-15 13:44:37 +00:00
// update finished date if enabled
if ( this . plugin . settings . autoUpdateFinishedDate ) {
if ( frontmatter [ "finished" ] !== today ) {
frontmatter [ "finished" ] = today ;
needsUpdate = true ;
2025-04-15 11:25:00 +00:00
2025-04-15 13:44:37 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Updating finished date to ${ today } in file: ` ,
file . path
) ;
}
2025-04-15 11:25:00 +00:00
}
}
2025-04-15 13:44:37 +00:00
return needsUpdate ;
}
) ;
2025-03-19 11:33:52 +00:00
} catch ( error ) {
console . error ( "Error updating file metadata:" , error ) ;
if ( this . plugin . settings . showDebugInfo ) {
new Notice (
` Error updating metadata for ${ file . basename } : ${ error . message } `
) ;
}
}
}
2025-03-20 10:59:28 +00:00
/ * *
* Handle Kanban board integration with files
* Updates the position of a note card in Kanban boards when tasks are completed or status changes
* /
async updateKanbanBoards (
2025-03-20 09:43:08 +00:00
file : TFile ,
2025-03-20 10:59:28 +00:00
completedTasks : number ,
totalTasks : number
) {
2025-03-19 12:25:02 +00:00
try {
2025-03-20 10:59:28 +00:00
// Only proceed if Kanban integration is enabled
if ( ! this . plugin . settings . autoUpdateKanban || totalTasks === 0 ) {
return ;
2025-03-19 12:25:02 +00:00
}
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// Calculate the current status based on progress
let currentStatus = this . calculateStatusFromProgress (
completedTasks ,
totalTasks
) ;
2025-03-20 07:21:00 +00:00
2025-04-15 13:44:37 +00:00
// Wait for MetadataCache to update
await this . waitForCacheUpdate ( file ) ;
2025-03-20 10:59:28 +00:00
// Get status from YAML frontmatter if available - more accurate
let statusFromYaml = await this . getStatusFromYaml ( file ) ;
if ( statusFromYaml ) {
currentStatus = statusFromYaml ;
2025-03-20 07:21:00 +00:00
2025-03-19 12:25:02 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-03-20 07:21:00 +00:00
console . log (
2025-03-20 10:59:28 +00:00
` Using status from YAML: ${ currentStatus } instead of calculated status `
2025-03-20 07:21:00 +00:00
) ;
2025-03-19 12:25:02 +00:00
}
}
2025-03-16 19:20:48 +00:00
2025-03-20 10:59:28 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Searching for Kanban boards that might contain ${ file . path } ... `
) ;
console . log (
` Current status is: ${ currentStatus } ( ${ completedTasks } / ${ totalTasks } tasks) `
) ;
}
2025-03-16 19:20:48 +00:00
2025-03-20 10:59:28 +00:00
// Get and process Kanban boards
const updatedBoardCount = await this . processKanbanBoards (
file ,
currentStatus
) ;
2025-03-16 19:20:48 +00:00
2025-03-20 10:59:28 +00:00
if ( updatedBoardCount > 0 ) {
new Notice (
` Updated ${ updatedBoardCount } Kanban board ${
updatedBoardCount > 1 ? "s" : ""
} to move $ { file . basename } to $ { currentStatus } column `
) ;
}
} catch ( error ) {
console . error ( "Error updating Kanban boards:" , error ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
new Notice ( ` Error updating Kanban boards: ${ error . message } ` ) ;
}
2025-03-16 11:34:29 +00:00
}
}
2025-03-19 11:33:52 +00:00
2025-03-20 10:59:28 +00:00
/ * *
* Calculate status based on task progress
* /
private calculateStatusFromProgress (
completedTasks : number ,
totalTasks : number
) : string {
if ( totalTasks === 0 ) {
return this . plugin . settings . statusTodo ;
} else if ( completedTasks === 0 ) {
return this . plugin . settings . statusTodo ;
} else if ( completedTasks === totalTasks ) {
return this . plugin . settings . statusCompleted ;
} else {
return this . plugin . settings . statusInProgress ;
2025-03-19 11:33:52 +00:00
}
}
2025-03-20 07:21:00 +00:00
/ * *
2025-03-20 10:59:28 +00:00
* Process all Kanban boards that might reference the target file
* Returns the number of boards that were updated
2025-03-20 07:21:00 +00:00
* /
2025-03-20 10:59:28 +00:00
private async processKanbanBoards (
file : TFile ,
currentStatus : string
) : Promise < number > {
2025-04-15 11:25:00 +00:00
// Skip plugin files and obvious Kanban files to avoid self-reference issues
2025-03-24 09:40:04 +00:00
const filePath = file . path . toLowerCase ( ) ;
2025-04-15 11:25:00 +00:00
const configDir = this . plugin . app . vault . configDir . toLowerCase ( ) ;
if (
filePath . includes ( ` ${ configDir } /plugins/progress-tracker ` ) ||
filePath . includes ( "kanban" )
) {
2025-03-24 09:40:04 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-04-15 11:25:00 +00:00
console . log (
` Skipping plugin or kanban file for kanban processing: ${ file . path } `
) ;
2025-03-24 09:40:04 +00:00
}
return 0 ;
}
2025-05-16 05:10:36 +00:00
let updatedBoardCount = 0 ;
2025-05-16 05:31:46 +00:00
// If there is a target board selected in settings, only process that board
2025-05-16 05:10:36 +00:00
if ( this . plugin . settings . autoAddKanbanBoard ) {
const targetBoard = this . plugin . app . vault . getAbstractFileByPath (
this . plugin . settings . autoAddKanbanBoard
) ;
if ( targetBoard instanceof TFile ) {
// Skip if trying to update the target file itself
if ( targetBoard . path === file . path ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Skipping target board as it's the file being updated: ${ file . path } `
) ;
}
return 0 ;
}
// Read and process the target board
2025-12-04 09:22:45 +00:00
const boardContent = await this . plugin . app . vault . read (
targetBoard
) ;
2025-05-16 05:10:36 +00:00
// Skip if not a Kanban board or doesn't reference our file
if (
! this . isKanbanBoard ( targetBoard ) ||
! this . containsFileReference ( boardContent , file )
) {
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Target board ${ targetBoard . path } is not a valid Kanban board or doesn't reference ${ file . path } `
) ;
}
return 0 ;
}
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Processing target Kanban board: ${ targetBoard . path } `
) ;
}
// Update the Kanban board
const updatedContent = await this . moveCardInKanbanBoard (
targetBoard ,
boardContent ,
file ,
currentStatus
) ;
// If the board was updated, increment the counter
if ( updatedContent !== boardContent ) {
updatedBoardCount ++ ;
}
return updatedBoardCount ;
} else {
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Target board not found: ${ this . plugin . settings . autoAddKanbanBoard } `
) ;
}
return 0 ;
}
}
2025-05-16 05:31:46 +00:00
// If no target board is set, search all possible boards
2025-05-16 05:10:36 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` No target board set, searching all potential Kanban boards... `
) ;
}
2025-03-20 10:59:28 +00:00
// Get all markdown files that might be Kanban boards
const markdownFiles = this . plugin . app . vault . getMarkdownFiles ( ) ;
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// Check each potential Kanban board file
for ( const boardFile of markdownFiles ) {
// Skip checking the current file itself
if ( boardFile . path === file . path ) continue ;
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// Read the content of the potential Kanban board
const boardContent = await this . plugin . app . vault . read ( boardFile ) ;
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// Skip if not a Kanban board or doesn't reference our file
if (
2025-04-15 13:44:37 +00:00
! this . isKanbanBoard ( boardFile ) ||
2025-03-20 10:59:28 +00:00
! this . containsFileReference ( boardContent , file )
)
continue ;
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Found Kanban board " ${ boardFile . path } " that references " ${ file . path } " `
) ;
2025-03-20 07:21:00 +00:00
}
2025-03-20 10:59:28 +00:00
// Update the Kanban board by moving the card
const updatedContent = await this . moveCardInKanbanBoard (
boardFile ,
boardContent ,
file ,
currentStatus
) ;
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
// If the board was updated, increment the counter
if ( updatedContent !== boardContent ) {
updatedBoardCount ++ ;
}
2025-03-20 07:21:00 +00:00
}
2025-03-20 10:59:28 +00:00
return updatedBoardCount ;
}
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
/ * *
* Safely escape regex special characters in a string
* Used by multiple methods that need to create regex patterns
* /
private escapeRegExp ( string : string ) : string {
if ( ! string ) return "" ;
return string . replace ( /[.*+?^${}()|[\]\\]/g , "\\$&" ) ;
}
2025-03-20 07:21:00 +00:00
2025-03-20 10:59:28 +00:00
/ * *
* Check if a Kanban board content contains a reference to the given file
* /
private containsFileReference ( boardContent : string , file : TFile ) : boolean {
// Different ways a file might be referenced in a Kanban board
const filePath = file . path ;
const filePathWithoutExtension = filePath . replace ( /\.md$/ , "" ) ;
const fileName = file . basename ;
2025-05-16 04:51:52 +00:00
// First try to find exact matches in Obsidian-style links
const obsidianLinks = this . extractObsidianLinks ( boardContent ) ;
for ( const link of obsidianLinks ) {
const { path , alias } = link ;
2025-12-04 09:22:45 +00:00
if (
path === fileName ||
path === filePathWithoutExtension ||
path === filePath
) {
2025-05-16 04:51:52 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found exact Obsidian link match for ${ fileName } : ${ path } `
) ;
2025-05-16 04:51:52 +00:00
}
return true ;
}
}
// Then check for Markdown-style links
const markdownLinks = this . extractMarkdownLinks ( boardContent ) ;
for ( const link of markdownLinks ) {
const { text , url } = link ;
if ( url === filePath || url === filePathWithoutExtension ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found exact Markdown link match for ${ fileName } : ${ url } `
) ;
2025-05-16 04:51:52 +00:00
}
return true ;
}
}
// Finally check for exact filepath mentions (with strict boundaries)
2025-12-04 09:22:45 +00:00
const filepathPattern = new RegExp (
` (?:^| \\ s| \\ () ${ this . escapeRegExp ( filePath ) } (?: $ | \\ s| \\ )) ` ,
"i"
) ;
2025-05-16 04:51:52 +00:00
if ( filepathPattern . test ( boardContent ) ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Found exact filepath match for ${ filePath } ` ) ;
}
return true ;
}
return false ;
}
/ * *
* Extract all Obsidian - style links from content
* Returns array of { path , alias } objects
* /
2025-12-04 09:22:45 +00:00
private extractObsidianLinks (
content : string
) : Array < { path : string ; alias? : string } > {
const links : Array < { path : string ; alias? : string } > = [ ] ;
2025-05-16 04:51:52 +00:00
const linkPattern = /\[\[(.*?)\]\]/g ;
let match ;
while ( ( match = linkPattern . exec ( content ) ) !== null ) {
const [ _ , linkContent ] = match ;
2025-12-04 09:22:45 +00:00
const [ path , alias ] = linkContent . split ( "|" ) . map ( ( s ) = > s . trim ( ) ) ;
2025-05-16 04:51:52 +00:00
links . push ( { path , alias } ) ;
}
if ( this . plugin . settings . showDebugInfo ) {
console . log ( "Extracted Obsidian links:" , links ) ;
}
return links ;
}
/ * *
* Extract all Markdown - style links from content
* Returns array of { text , url } objects
* /
2025-12-04 09:22:45 +00:00
private extractMarkdownLinks (
content : string
) : Array < { text : string ; url : string } > {
const links : Array < { text : string ; url : string } > = [ ] ;
2025-05-16 04:51:52 +00:00
const linkPattern = /\[(.*?)\]\((.*?)\)/g ;
let match ;
while ( ( match = linkPattern . exec ( content ) ) !== null ) {
const [ _ , text , url ] = match ;
links . push ( { text : text.trim ( ) , url : url.trim ( ) } ) ;
}
if ( this . plugin . settings . showDebugInfo ) {
console . log ( "Extracted Markdown links:" , links ) ;
}
return links ;
2025-03-20 07:21:00 +00:00
}
/ * *
* Move a card in a Kanban board to the appropriate column based on status
* /
async moveCardInKanbanBoard (
boardFile : TFile ,
boardContent : string ,
fileToMove : TFile ,
targetStatus : string
) : Promise < string > {
try {
2025-04-15 13:58:15 +00:00
// Wait for MetadataCache to update
await this . waitForCacheUpdate ( fileToMove ) ;
2025-03-20 07:21:00 +00:00
// Parse the Kanban board structure
2025-04-15 13:58:15 +00:00
const kanbanColumns = await this . parseKanbanBoard ( boardFile ) ;
2025-03-20 07:21:00 +00:00
if ( ! kanbanColumns || Object . keys ( kanbanColumns ) . length === 0 ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-03-20 09:43:08 +00:00
console . log (
` Could not parse Kanban board structure in ${ boardFile . path } `
) ;
2025-03-20 07:21:00 +00:00
}
return boardContent ;
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// Determine the target column name based on settings
let targetColumnName : string | undefined ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
if ( this . plugin . settings . kanbanSyncWithStatus ) {
// When syncing with status, use exact status name as the column name
targetColumnName = Object . keys ( kanbanColumns ) . find (
2025-03-20 09:43:08 +00:00
( name ) = > name . toLowerCase ( ) === targetStatus . toLowerCase ( )
2025-03-20 07:21:00 +00:00
) ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// If the exact status name isn't found, try a fuzzy match
if ( ! targetColumnName ) {
2025-03-20 09:43:08 +00:00
targetColumnName = this . findClosestColumnName (
Object . keys ( kanbanColumns ) ,
targetStatus
) ;
2025-03-20 07:21:00 +00:00
}
} else {
// Legacy behavior: When not syncing, use the completed column setting, but only for completed tasks
if ( targetStatus === this . plugin . settings . statusCompleted ) {
targetColumnName = Object . keys ( kanbanColumns ) . find (
2025-03-20 09:43:08 +00:00
( name ) = >
name . toLowerCase ( ) ===
this . plugin . settings . kanbanCompletedColumn . toLowerCase ( )
2025-03-20 07:21:00 +00:00
) ;
}
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// If no matching column found, log and return original content
if ( ! targetColumnName ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-03-20 09:43:08 +00:00
console . log (
` Could not find column for status " ${ targetStatus } " in Kanban board ${ boardFile . path } `
) ;
console . log (
` Available columns: ${ Object . keys ( kanbanColumns ) . join (
", "
) } `
) ;
2025-03-20 07:21:00 +00:00
}
return boardContent ;
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-03-20 09:43:08 +00:00
console . log (
` Target column for status " ${ targetStatus } " is " ${ targetColumnName } " `
) ;
2025-03-20 07:21:00 +00:00
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
// Process each column to find and move the card
2025-03-20 07:21:00 +00:00
let cardMoved = false ;
let newContent = boardContent ;
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
// Split content into lines for more accurate processing
const lines = newContent . split ( "\n" ) ;
let currentColumn = "" ;
let cardStartIndex = - 1 ;
let cardEndIndex = - 1 ;
// First pass: Find the exact card that references our file
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
// Check for column header
if ( line . startsWith ( "## " ) ) {
currentColumn = line . substring ( 3 ) . trim ( ) ;
2025-03-20 07:21:00 +00:00
continue ;
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
// Skip if we're already in target column
2025-12-04 09:22:45 +00:00
if (
currentColumn . toLowerCase ( ) ===
targetColumnName . toLowerCase ( )
) {
2025-05-16 04:51:52 +00:00
continue ;
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
// Check if line is a card (starts with "- ")
if ( line . trim ( ) . startsWith ( "- " ) ) {
// Extract the card content
const cardContent = this . getCompleteCardContent ( lines , i ) ;
2025-12-04 09:22:45 +00:00
2025-03-20 07:21:00 +00:00
// Check if this card references our file
2025-12-04 09:22:45 +00:00
if (
this . isExactCardForFile ( cardContent . content , fileToMove )
) {
2025-05-16 04:51:52 +00:00
cardStartIndex = i ;
cardEndIndex = i + cardContent . lineCount - 1 ;
break ;
}
2025-12-04 09:22:45 +00:00
2025-05-16 04:51:52 +00:00
// Skip to end of card
i += cardContent . lineCount - 1 ;
}
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
// If we found the card, move it
if ( cardStartIndex !== - 1 && cardEndIndex !== - 1 ) {
// Extract the card content
2025-07-12 08:57:43 +00:00
let cardLines = lines . slice ( cardStartIndex , cardEndIndex + 1 ) ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Update checkbox states in the card if custom checkbox states are enabled
2025-12-04 09:22:45 +00:00
if (
this . plugin . settings . enableCustomCheckboxStates &&
targetColumnName
) {
2025-07-12 08:57:43 +00:00
const columnName = targetColumnName ; // Type assertion for TypeScript
2025-12-04 09:22:45 +00:00
cardLines = cardLines . map ( ( line ) = > {
return this . updateCheckboxStatesInCard (
line ,
columnName
) ;
2025-07-12 08:57:43 +00:00
} ) ;
}
2025-12-04 09:22:45 +00:00
2025-05-16 04:51:52 +00:00
// Remove the card from its current position
lines . splice ( cardStartIndex , cardEndIndex - cardStartIndex + 1 ) ;
// Find the target column and insert the card
let targetInsertIndex = - 1 ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
if ( lines [ i ] . startsWith ( ` ## ${ targetColumnName } ` ) ) {
targetInsertIndex = i + 1 ;
break ;
}
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
if ( targetInsertIndex !== - 1 ) {
// Insert the card at the beginning of the target column
lines . splice ( targetInsertIndex , 0 , . . . cardLines ) ;
cardMoved = true ;
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Moved card for ${ fileToMove . path } to column " ${ targetColumnName } " in ${ boardFile . path } `
2025-03-20 09:43:08 +00:00
) ;
2025-07-12 08:57:43 +00:00
if ( this . plugin . settings . enableCustomCheckboxStates ) {
2025-12-04 09:22:45 +00:00
const targetCheckboxState =
this . getCheckboxStateForColumn (
targetColumnName
) ;
console . log (
` Applied checkbox state " ${ targetCheckboxState } " to card `
) ;
2025-07-12 08:57:43 +00:00
}
2025-03-20 07:21:00 +00:00
}
}
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
// If card was moved, update the file
if ( cardMoved ) {
newContent = lines . join ( "\n" ) ;
2025-03-20 07:21:00 +00:00
await this . plugin . app . vault . modify ( boardFile , newContent ) ;
}
2025-03-20 09:43:08 +00:00
2025-05-16 04:51:52 +00:00
return newContent ;
2025-03-20 07:21:00 +00:00
} catch ( error ) {
console . error ( "Error moving card in Kanban board:" , error ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
return boardContent ;
}
}
2025-05-16 04:51:52 +00:00
/ * *
* Get the complete content of a card , including any sub - items
* /
2025-12-04 09:22:45 +00:00
private getCompleteCardContent (
lines : string [ ] ,
startIndex : number
) : { content : string ; lineCount : number } {
2025-05-16 04:51:52 +00:00
let content = lines [ startIndex ] ;
let lineCount = 1 ;
2025-12-04 09:22:45 +00:00
2025-05-16 04:51:52 +00:00
// Check subsequent lines for sub-items (indented)
for ( let i = startIndex + 1 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
2025-12-04 09:22:45 +00:00
if (
line . trim ( ) === "" ||
line . startsWith ( " " ) ||
line . startsWith ( "\t" )
) {
2025-05-16 04:51:52 +00:00
content += "\n" + line ;
lineCount ++ ;
} else {
break ;
}
}
2025-12-04 09:22:45 +00:00
2025-05-16 04:51:52 +00:00
return { content , lineCount } ;
}
/ * *
* Check if a card content exactly matches a file reference
* /
private isExactCardForFile ( cardContent : string , file : TFile ) : boolean {
// Extract all links from the card
const obsidianLinks = this . extractObsidianLinks ( cardContent ) ;
const markdownLinks = this . extractMarkdownLinks ( cardContent ) ;
const fileName = file . basename ;
const filePath = file . path ;
const filePathWithoutExtension = filePath . replace ( /\.md$/ , "" ) ;
// Check Obsidian links first
for ( const link of obsidianLinks ) {
const { path } = link ;
// Only match if it's an exact match with the full path or filename
2025-12-04 09:22:45 +00:00
if (
path === fileName ||
path === filePathWithoutExtension ||
path === filePath
) {
2025-05-16 04:51:52 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found exact Obsidian link match in card: ${ path } for file: ${ fileName } `
) ;
2025-05-16 04:51:52 +00:00
}
return true ;
}
}
// Then check Markdown links
for ( const link of markdownLinks ) {
const { url } = link ;
if ( url === filePath || url === filePathWithoutExtension ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-12-04 09:22:45 +00:00
console . log (
` Found exact Markdown link match in card: ${ url } for file: ${ fileName } `
) ;
2025-05-16 04:51:52 +00:00
}
return true ;
}
}
return false ;
}
2025-03-20 07:21:00 +00:00
/ * *
* Find the closest column name match for a given status
* This helps when Kanban columns don ' t exactly match status names
* /
2025-03-20 09:43:08 +00:00
findClosestColumnName (
columnNames : string [ ] ,
targetStatus : string
) : string | undefined {
2025-03-20 07:21:00 +00:00
const targetLower = targetStatus . toLowerCase ( ) ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// Define common variants of status names and their corresponding column names
const statusVariants : Record < string , string [ ] > = {
2025-03-20 09:43:08 +00:00
todo : [
"to do" ,
"todo" ,
"backlog" ,
"new" ,
"not started" ,
"pending" ,
"open" ,
"to-do" ,
] ,
"in progress" : [
"progress" ,
"doing" ,
"working" ,
"ongoing" ,
"started" ,
"in work" ,
"active" ,
"current" ,
"wip" ,
] ,
completed : [
"done" ,
"complete" ,
"finished" ,
"closed" ,
"resolved" ,
"ready" ,
"completed" ,
] ,
2025-03-20 07:21:00 +00:00
} ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// First try exact match (case-insensitive)
2025-03-20 09:43:08 +00:00
const exactMatch = columnNames . find (
( name ) = > name . toLowerCase ( ) === targetLower
) ;
2025-03-20 07:21:00 +00:00
if ( exactMatch ) return exactMatch ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// Special handling for common status values
if ( targetLower === this . plugin . settings . statusTodo . toLowerCase ( ) ) {
for ( const colName of columnNames ) {
const colNameLower = colName . toLowerCase ( ) ;
2025-03-20 09:43:08 +00:00
if (
statusVariants [ "todo" ] . some (
( v ) = > colNameLower . includes ( v ) || v === colNameLower
)
) {
2025-03-20 07:21:00 +00:00
return colName ;
}
}
2025-03-20 09:43:08 +00:00
} else if (
targetLower === this . plugin . settings . statusInProgress . toLowerCase ( )
) {
2025-03-20 07:21:00 +00:00
for ( const colName of columnNames ) {
const colNameLower = colName . toLowerCase ( ) ;
2025-03-20 09:43:08 +00:00
if (
statusVariants [ "in progress" ] . some (
( v ) = > colNameLower . includes ( v ) || v === colNameLower
)
) {
2025-03-20 07:21:00 +00:00
return colName ;
}
}
2025-03-20 09:43:08 +00:00
} else if (
targetLower === this . plugin . settings . statusCompleted . toLowerCase ( )
) {
2025-03-20 07:21:00 +00:00
for ( const colName of columnNames ) {
const colNameLower = colName . toLowerCase ( ) ;
2025-03-20 09:43:08 +00:00
if (
statusVariants [ "completed" ] . some (
( v ) = > colNameLower . includes ( v ) || v === colNameLower
)
) {
2025-03-20 07:21:00 +00:00
return colName ;
}
}
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// Then try fuzzy variant matching if status is not a standard one
for ( const [ status , variants ] of Object . entries ( statusVariants ) ) {
2025-03-20 09:43:08 +00:00
if (
variants . some (
( v ) = > targetLower . includes ( v ) || v . includes ( targetLower )
)
) {
2025-03-20 07:21:00 +00:00
for ( const colName of columnNames ) {
const colNameLower = colName . toLowerCase ( ) ;
2025-03-20 09:43:08 +00:00
if (
variants . some (
( v ) = >
colNameLower . includes ( v ) ||
v . includes ( colNameLower )
)
) {
2025-03-20 07:21:00 +00:00
return colName ;
}
}
}
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// If there's a direct word match, use that
for ( const colName of columnNames ) {
if ( colName . toLowerCase ( ) === targetLower ) {
return colName ;
}
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// If no matches are found, try to find any column with a partial match
for ( const colName of columnNames ) {
2025-03-20 09:43:08 +00:00
if (
colName . toLowerCase ( ) . includes ( targetLower ) ||
targetLower . includes ( colName . toLowerCase ( ) )
) {
2025-03-20 07:21:00 +00:00
return colName ;
}
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// If still no matches, and this is a "Todo" status with no matching column,
// return the first column as a likely match for the starting column
2025-03-20 09:43:08 +00:00
if (
targetLower === this . plugin . settings . statusTodo . toLowerCase ( ) &&
columnNames . length > 0
) {
2025-03-20 07:21:00 +00:00
return columnNames [ 0 ] ; // First column is often the "Todo" equivalent
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
// If no matches are found, return undefined
return undefined ;
}
/ * *
* Get the status from the file ' s YAML frontmatter
* /
async getStatusFromYaml ( file : TFile ) : Promise < string | null > {
2025-04-15 13:44:37 +00:00
// Get the file cache from MetadataCache
let fileCache = this . app . metadataCache . getFileCache ( file ) ;
2025-03-20 09:43:08 +00:00
2025-04-15 13:44:37 +00:00
// If no cache or frontmatter, log and return null
if ( ! fileCache ? . frontmatter ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` No frontmatter found for file: ${ file . path } ` ) ;
}
return null ;
}
2025-03-20 09:43:08 +00:00
2025-04-15 13:44:37 +00:00
try {
// Retrieve status from frontmatter
const status = fileCache . frontmatter [ "status" ] ;
if ( typeof status === "string" && status . trim ( ) ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Status found for ${ file . path } : ${ status } ` ) ;
}
return status . trim ( ) ;
}
2025-03-20 09:43:08 +00:00
2025-04-15 13:44:37 +00:00
// Log if no valid status is found
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` No valid status in frontmatter for file: ${ file . path } `
) ;
2025-03-20 07:21:00 +00:00
}
} catch ( error ) {
2025-04-15 13:44:37 +00:00
// Log any errors during frontmatter access
console . error (
` Error accessing frontmatter for ${ file . path } : ` ,
error
) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
2025-03-20 07:21:00 +00:00
}
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
return null ;
}
2025-03-20 09:43:08 +00:00
2025-04-15 13:44:37 +00:00
// Utility function to wait for MetadataCache to update for a specific file
async waitForCacheUpdate (
file : TFile ,
timeoutMs : number = 1000
) : Promise < void > {
return new Promise ( ( resolve , reject ) = > {
// Set a timeout to prevent hanging
const timeout = setTimeout ( ( ) = > {
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Timeout waiting for cache update for ${ file . path } `
) ;
}
resolve ( ) ;
} , timeoutMs ) ;
// Listen for cache changes
const handler = ( updatedFile : TFile ) = > {
if ( updatedFile . path === file . path ) {
// Cache updated, cleanup and resolve
this . app . metadataCache . off ( "changed" , handler ) ;
clearTimeout ( timeout ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Cache updated for ${ file . path } ` ) ;
}
resolve ( ) ;
}
} ;
// Register the listener
this . app . metadataCache . on ( "changed" , handler ) ;
// Trigger a cache refresh (optional, depends on Obsidian version)
// this.app.metadataCache.trigger("changed", file);
} ) ;
}
2025-03-20 09:43:08 +00:00
/ * *
* Parse Kanban board structure into columns and items with improved accuracy
* /
2025-04-15 13:58:15 +00:00
private async parseKanbanBoard (
file : TFile
) : Promise < Record < string , { items : Array < { text : string } > } >> {
2025-03-20 09:43:08 +00:00
const kanban : Record < string , { items : Array < { text : string } > } > = { } ;
2025-04-15 13:58:15 +00:00
try {
// Use MetadataCache to get frontmatter and headers
const fileCache = this . app . metadataCache . getFileCache ( file ) ;
if ( ! fileCache ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` No cache found for file: ${ file . path } ` ) ;
}
return kanban ;
}
2025-03-20 09:43:08 +00:00
2025-04-15 13:58:15 +00:00
// Check if this is a Kanban plugin file using frontmatter
const isKanbanPlugin =
fileCache . frontmatter ? . [ "kanban-plugin" ] === "basic" ;
2025-03-20 09:43:08 +00:00
2025-04-15 13:58:15 +00:00
// Get H2 headers (level 2) to identify columns
const columnHeaders =
fileCache . headings ? . filter ( ( h ) = > h . level === 2 ) || [ ] ;
if ( columnHeaders . length < 1 ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` No H2 headers found in file: ${ file . path } ` ) ;
}
return kanban ;
}
2025-03-20 09:43:08 +00:00
2025-04-15 13:58:15 +00:00
// Read file content only when necessary to extract items
const content = await this . plugin . app . vault . read ( file ) ;
// Process each column
for ( let i = 0 ; i < columnHeaders . length ; i ++ ) {
const columnHeader = columnHeaders [ i ] ;
const columnName = columnHeader . heading . trim ( ) ;
kanban [ columnName ] = { items : [ ] } ;
// Determine column boundaries using header positions
const columnStart = columnHeader . position . start . offset ;
const columnEnd =
i < columnHeaders . length - 1
? columnHeaders [ i + 1 ] . position . start . offset
: content . length ;
// Extract column content
const columnContent = content
. substring (
columnStart + columnHeader . heading . length + 4 ,
columnEnd
)
. trim ( ) ; // +4 accounts for "## " and newline
// Extract items based on format
if ( isKanbanPlugin ) {
this . extractKanbanPluginItems (
columnContent ,
kanban [ columnName ] . items
) ;
} else {
this . extractMarkdownItems (
columnContent ,
kanban [ columnName ] . items
) ;
}
2025-03-20 09:43:08 +00:00
}
2025-04-15 13:58:15 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-03-20 09:43:08 +00:00
console . log (
2025-04-15 13:58:15 +00:00
` Parsed Kanban board ${ file . path } with columns: ` ,
Object . keys ( kanban )
2025-03-20 09:43:08 +00:00
) ;
2025-04-15 13:58:15 +00:00
Object . entries ( kanban ) . forEach ( ( [ column , data ] ) = > {
console . log (
` Column " ${ column } " has ${ data . items . length } items `
) ;
} ) ;
}
} catch ( error ) {
console . error ( ` Error parsing Kanban board ${ file . path } : ` , error ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
2025-03-20 09:43:08 +00:00
}
return kanban ;
}
/ * *
* Extract items from Kanban plugin format
* /
private extractKanbanPluginItems (
columnContent : string ,
items : Array < { text : string } >
) {
// Split by top-level list items (those that start with "- " at beginning of line)
const listItemsRaw = columnContent . split ( /^- /m ) . slice ( 1 ) ;
for ( const rawItem of listItemsRaw ) {
const itemText = "- " + rawItem . trim ( ) ;
items . push ( { text : itemText } ) ;
}
}
/ * *
* Extract items from regular markdown format
* /
private extractMarkdownItems (
columnContent : string ,
items : Array < { text : string } >
) {
// Each item starts with "- " at the beginning of a line
// And continues until the next item or end of content
let lines = columnContent . split ( "\n" ) ;
let currentItem = "" ;
let inItem = false ;
for ( const line of lines ) {
if ( line . trim ( ) . startsWith ( "- " ) ) {
// If we were already in an item, save the previous one
if ( inItem ) {
items . push ( { text : currentItem.trim ( ) } ) ;
}
// Start a new item
currentItem = line ;
inItem = true ;
} else if ( inItem ) {
// Continue current item
currentItem += "\n" + line ;
}
}
// Add the last item if there is one
if ( inItem ) {
items . push ( { text : currentItem.trim ( ) } ) ;
}
}
2022-04-15 18:13:31 +00:00
2025-03-20 10:59:28 +00:00
/ * *
* Check if content appears to be a Kanban board
* This method is necessary for the processKanbanBoards function
* /
2025-04-15 13:44:37 +00:00
private isKanbanBoard ( file : TFile ) : boolean {
try {
2025-04-15 14:04:44 +00:00
// Use MetadataCache to get file cache
const fileCache = this . app . metadataCache . getFileCache ( file ) ;
if ( ! fileCache ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` No cache found for file: ${ file . path } ` ) ;
}
return false ;
}
2025-03-20 10:59:28 +00:00
2025-04-15 14:04:44 +00:00
// Check for Kanban plugin frontmatter
if ( fileCache . frontmatter ? . [ "kanban-plugin" ] === "basic" ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Detected Kanban plugin board: ${ file . path } ` ) ;
}
2025-04-15 13:44:37 +00:00
return true ;
}
2025-03-20 10:59:28 +00:00
2025-04-15 14:04:44 +00:00
// Check for Kanban-like structure via headers
const headers = fileCache . headings || [ ] ;
2025-04-15 13:44:37 +00:00
if ( headers . length < 2 ) {
2025-04-15 14:04:44 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Insufficient headers in file: ${ file . path } ` ) ;
}
2025-04-15 13:44:37 +00:00
return false ;
2025-03-20 10:59:28 +00:00
}
2025-04-15 13:44:37 +00:00
const commonKanbanNames = [
"todo" ,
"to do" ,
"to-do" ,
"backlog" ,
"new" ,
"ideas" ,
"inbox" ,
"in progress" ,
"doing" ,
"working" ,
"current" ,
"ongoing" ,
"done" ,
"complete" ,
"completed" ,
"finished" ,
"blocked" ,
"waiting" ,
] ;
2025-03-20 10:59:28 +00:00
2025-04-15 13:44:37 +00:00
let kanbanColumnCount = 0 ;
2025-04-15 14:04:44 +00:00
const completedColumnLower =
this . plugin . settings . kanbanCompletedColumn . toLowerCase ( ) ;
2025-04-15 13:44:37 +00:00
2025-04-15 14:04:44 +00:00
for ( const header of headers ) {
if ( header . level !== 2 ) continue ;
const columnName = header . heading . toLowerCase ( ) ;
2025-03-20 10:59:28 +00:00
2025-04-15 14:04:44 +00:00
if (
commonKanbanNames . some ( ( name ) = >
columnName . includes ( name )
) ||
columnName === completedColumnLower
) {
kanbanColumnCount ++ ;
}
}
2025-03-20 10:59:28 +00:00
2025-04-15 14:04:44 +00:00
const isKanban = kanbanColumnCount >= 2 ;
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` File ${ file . path } is ${
isKanban ? "" : "not "
} a Kanban board ` +
` (columns detected: ${ kanbanColumnCount } ) `
) ;
2025-03-20 10:59:28 +00:00
}
2025-04-15 14:04:44 +00:00
return isKanban ;
2025-04-15 13:44:37 +00:00
} catch ( error ) {
2025-04-15 14:04:44 +00:00
console . error (
` Error checking if ${ file . path } is a Kanban board: ` ,
error
) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
2025-04-15 13:44:37 +00:00
return false ;
}
2025-03-20 10:59:28 +00:00
}
2025-03-24 09:40:04 +00:00
/ * *
* Add a file to the specified Kanban board if it ' s not already there
* /
async addFileToKanbanBoard ( file : TFile ) : Promise < boolean > {
try {
// Skip if auto-add setting is disabled or board path is empty
2025-04-15 11:25:00 +00:00
if (
! this . plugin . settings . autoAddToKanban ||
! this . plugin . settings . autoAddKanbanBoard
) {
2025-03-24 09:40:04 +00:00
return false ;
}
2025-04-15 11:25:00 +00:00
// Skip plugin files and Kanban board files to avoid self-reference
2025-03-24 09:40:04 +00:00
const filePath = file . path . toLowerCase ( ) ;
2025-04-15 11:25:00 +00:00
const configDir = this . plugin . app . vault . configDir . toLowerCase ( ) ;
if (
filePath . includes ( ` ${ configDir } /plugins/progress-tracker ` ) ||
filePath . includes ( "kanban" ) ||
filePath === this . plugin . settings . autoAddKanbanBoard
) {
2025-03-24 09:40:04 +00:00
if ( this . plugin . settings . showDebugInfo ) {
console . log ( ` Skipping plugin or kanban file: ${ file . path } ` ) ;
}
return false ;
}
// Get the Kanban board file
const boardPath = this . plugin . settings . autoAddKanbanBoard ;
2025-04-15 11:25:00 +00:00
const kanbanFile =
this . plugin . app . vault . getAbstractFileByPath ( boardPath ) ;
2025-03-24 09:40:04 +00:00
if ( ! kanbanFile || ! ( kanbanFile instanceof TFile ) ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-04-15 11:25:00 +00:00
console . log (
` Could not find Kanban board at path: ${ boardPath } `
) ;
2025-03-24 09:40:04 +00:00
}
return false ;
}
2025-04-15 11:25:00 +00:00
// Skip if trying to add the kanban board to itself
2025-03-24 09:40:04 +00:00
if ( file . path === kanbanFile . path ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-04-15 11:25:00 +00:00
console . log (
` Skipping adding kanban board to itself: ${ file . path } `
) ;
2025-03-24 09:40:04 +00:00
}
return false ;
}
// Read the board content
2025-04-15 14:38:12 +00:00
const boardContent = await this . plugin . app . vault . read ( kanbanFile ) ;
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Skip if this is not a Kanban board
2025-04-15 13:44:37 +00:00
if ( ! this . isKanbanBoard ( kanbanFile ) ) {
2025-03-24 09:40:04 +00:00
if ( this . plugin . settings . showDebugInfo ) {
2025-04-15 11:25:00 +00:00
console . log (
` File at path ${ boardPath } is not a Kanban board `
) ;
2025-03-24 09:40:04 +00:00
}
return false ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Check if the file is already referenced in the board
if ( this . containsFileReference ( boardContent , file ) ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-04-15 11:25:00 +00:00
console . log (
` File ${ file . path } is already in Kanban board ${ boardPath } `
) ;
2025-03-24 09:40:04 +00:00
}
return false ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Get the target column name
2025-04-15 11:25:00 +00:00
const targetColumn =
this . plugin . settings . autoAddKanbanColumn || "Todo" ;
2025-04-15 13:58:15 +00:00
// Wait for MetadataCache to update
await this . waitForCacheUpdate ( file ) ;
2025-03-24 09:40:04 +00:00
// Parse the Kanban board to find the column
2025-04-15 13:58:15 +00:00
const kanbanColumns = await this . parseKanbanBoard ( kanbanFile ) ;
2025-03-24 09:40:04 +00:00
if ( ! kanbanColumns || Object . keys ( kanbanColumns ) . length === 0 ) {
if ( this . plugin . settings . showDebugInfo ) {
2025-04-15 11:25:00 +00:00
console . log (
` Could not parse Kanban board structure in ${ boardPath } `
) ;
2025-03-24 09:40:04 +00:00
}
return false ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Find the exact or closest column match
let targetColumnName = Object . keys ( kanbanColumns ) . find (
( name ) = > name . toLowerCase ( ) === targetColumn . toLowerCase ( )
) ;
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
if ( ! targetColumnName ) {
targetColumnName = this . findClosestColumnName (
Object . keys ( kanbanColumns ) ,
targetColumn
) ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
if ( ! targetColumnName ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Could not find column " ${ targetColumn } " in Kanban board ${ boardPath } `
) ;
}
return false ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Find the position to insert the card
const columnRegex = new RegExp (
` ## ${ this . escapeRegExp ( targetColumnName ) } \\ s* \\ n `
) ;
const columnMatch = boardContent . match ( columnRegex ) ;
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
if ( ! columnMatch ) {
if ( this . plugin . settings . showDebugInfo ) {
console . log (
` Could not find column " ${ targetColumnName } " in Kanban board content `
) ;
}
return false ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
const insertPosition = columnMatch . index ! + columnMatch [ 0 ] . length ;
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Create card text with link to the file
2025-07-12 08:57:43 +00:00
let cardText = ` - [[ ${ file . basename } ]] \ n ` ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Apply custom checkbox state if enabled
if ( this . plugin . settings . enableCustomCheckboxStates ) {
2025-12-04 09:22:45 +00:00
const checkboxState =
this . getCheckboxStateForColumn ( targetColumnName ) ;
2025-07-12 08:57:43 +00:00
cardText = ` - ${ checkboxState } [[ ${ file . basename } ]] \ n ` ;
}
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Insert the card
2025-04-15 11:25:00 +00:00
const newContent =
2025-03-24 09:40:04 +00:00
boardContent . substring ( 0 , insertPosition ) +
cardText +
boardContent . substring ( insertPosition ) ;
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Update the file
2025-04-15 14:38:12 +00:00
await this . plugin . app . vault . modify ( kanbanFile , newContent ) ;
2025-04-15 11:25:00 +00:00
2025-03-24 09:40:04 +00:00
// Show notice
2025-04-15 11:25:00 +00:00
new Notice (
` Added ${ file . basename } to " ${ targetColumnName } " column in ${ kanbanFile . basename } `
) ;
2025-03-24 09:40:04 +00:00
return true ;
} catch ( error ) {
console . error ( "Error adding file to Kanban board:" , error ) ;
if ( this . plugin . settings . showDebugInfo ) {
console . error ( "Error details:" , error ) ;
}
return false ;
}
}
2025-07-12 08:57:43 +00:00
/ * *
* Get checkbox state for a specific kanban column
* /
private getCheckboxStateForColumn ( columnName : string ) : string {
if ( ! this . plugin . settings . enableCustomCheckboxStates ) {
return "[ ]" ; // Default unchecked state
}
const mapping = this . plugin . settings . kanbanColumnCheckboxMappings . find (
2025-12-04 09:22:45 +00:00
( m ) = > m . columnName . toLowerCase ( ) === columnName . toLowerCase ( )
2025-07-12 08:57:43 +00:00
) ;
return mapping ? mapping . checkboxState : "[ ]" ;
}
/ * *
* Update checkbox states in card content based on target column
* Only updates the main card checkbox , preserving sub - items and nested checkboxes
* /
2025-12-04 09:22:45 +00:00
private updateCheckboxStatesInCard (
cardContent : string ,
targetColumnName : string
) : string {
2025-07-12 08:57:43 +00:00
if ( ! this . plugin . settings . enableCustomCheckboxStates ) {
return cardContent ;
}
2025-12-04 09:22:45 +00:00
const targetCheckboxState =
this . getCheckboxStateForColumn ( targetColumnName ) ;
2025-07-12 08:57:43 +00:00
// Split content into lines to process only the first line (main card)
2025-12-04 09:22:45 +00:00
const lines = cardContent . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
if ( lines . length === 0 ) return cardContent ;
// Pattern to match various checkbox states: - [ ], - [x], - [/], - [>], etc.
// Remove global flag to only match once per line
const checkboxPattern = /^(\s*- )\[[^\]]*\](.*)$/ ;
2025-12-04 09:22:45 +00:00
2025-07-12 08:57:43 +00:00
// Only update the first line if it matches the pattern (main card line)
if ( checkboxPattern . test ( lines [ 0 ] ) ) {
const originalFirstLine = lines [ 0 ] ;
2025-12-04 09:22:45 +00:00
lines [ 0 ] = lines [ 0 ] . replace (
checkboxPattern ,
( match , prefix , suffix ) = > {
return ` ${ prefix } ${ targetCheckboxState } ${ suffix } ` ;
}
) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
if (
this . plugin . settings . showDebugInfo &&
lines [ 0 ] !== originalFirstLine
) {
console . log (
` Updated checkbox states in card for column " ${ targetColumnName } ": ` ,
{
original : originalFirstLine ,
updated : lines [ 0 ] ,
targetState : targetCheckboxState ,
}
) ;
2025-07-12 08:57:43 +00:00
}
}
// Join lines back together, preserving sub-items unchanged
2025-12-04 09:22:45 +00:00
return lines . join ( "\n" ) ;
2025-07-12 08:57:43 +00:00
}
/ * *
* Count tasks with different checkbox states
* /
2025-12-04 09:22:45 +00:00
private countTasksByCheckboxState ( content : string ) : {
[ state : string ] : number ;
} {
2025-07-12 08:57:43 +00:00
const taskCounts : { [ state : string ] : number } = { } ;
2025-12-04 09:22:45 +00:00
const lines = content . split ( "\n" ) ;
2025-07-12 08:57:43 +00:00
for ( const line of lines ) {
const match = line . trim ( ) . match ( /^- \[([^\]]*)\]/ ) ;
if ( match ) {
const state = match [ 1 ] ;
taskCounts [ state ] = ( taskCounts [ state ] || 0 ) + 1 ;
}
}
return taskCounts ;
}
2025-04-15 11:25:00 +00:00
}
2025-03-20 10:59:28 +00:00
2025-04-15 11:25:00 +00:00
class TaskProgressBarSettingTab extends PluginSettingTab {
plugin : TaskProgressBarPlugin ;
2022-04-15 18:13:31 +00:00
2025-03-16 09:26:27 +00:00
constructor ( app : App , plugin : TaskProgressBarPlugin ) {
2022-04-15 18:13:31 +00:00
super ( app , plugin ) ;
this . plugin = plugin ;
2025-04-15 11:25:00 +00:00
}
2022-04-15 18:13:31 +00:00
display ( ) : void {
2025-03-16 09:26:27 +00:00
const { containerEl } = this ;
2022-04-15 18:13:31 +00:00
containerEl . empty ( ) ;
2025-03-20 11:32:16 +00:00
// Add Dataview status information
2025-03-16 19:20:48 +00:00
const dataviewStatus = containerEl . createDiv ( {
cls : "dataview-status" ,
} ) ;
2025-03-16 09:26:27 +00:00
if ( this . plugin . dvAPI ) {
2025-03-16 19:20:48 +00:00
dataviewStatus . createEl ( "p" , {
text : "✅ Dataview API is available" ,
cls : "dataview-available" ,
2025-03-16 09:26:27 +00:00
} ) ;
} else {
2025-03-16 19:20:48 +00:00
dataviewStatus . createEl ( "p" , {
text : "❌ Dataview API is not available" ,
cls : "dataview-unavailable" ,
2025-03-16 09:26:27 +00:00
} ) ;
2025-03-16 19:20:48 +00:00
2025-03-20 11:32:16 +00:00
// Add button to check for Dataview again
2025-03-16 19:20:48 +00:00
const checkButton = dataviewStatus . createEl ( "button" , {
text : "Check for Dataview" ,
cls : "mod-cta" ,
2025-03-16 09:26:27 +00:00
} ) ;
2025-03-16 19:20:48 +00:00
checkButton . addEventListener ( "click" , ( ) = > {
2025-03-16 09:26:27 +00:00
this . plugin . checkDataviewAPI ( ) ;
if ( this . plugin . dvAPI ) {
2025-03-16 19:20:48 +00:00
new Notice ( "Dataview API found!" ) ;
2025-03-16 09:26:27 +00:00
this . display ( ) ; // Refresh settings tab
} else {
2025-03-16 19:20:48 +00:00
new Notice (
"Dataview API not found. Make sure Dataview plugin is installed and enabled."
) ;
2025-03-16 09:26:27 +00:00
}
} ) ;
}
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Show debug info" )
2025-03-16 19:20:48 +00:00
. setDesc (
"Show debug information in the sidebar to help troubleshoot task counting issues"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . showDebugInfo )
. onChange ( async ( value ) = > {
this . plugin . settings . showDebugInfo = value ;
await this . plugin . saveSettings ( ) ;
2025-03-20 11:32:16 +00:00
// Update sidebar if open - use public method
2025-03-16 19:20:48 +00:00
const currentFile = this . app . workspace . getActiveFile ( ) ;
if ( currentFile ) {
2025-03-20 11:32:16 +00:00
// Use public method to update UI
2025-03-16 19:20:48 +00:00
this . plugin . checkDataviewAPI ( ) ;
}
} )
) ;
2025-03-16 18:17:32 +00:00
// Animation settings section
2025-04-15 14:38:12 +00:00
new Setting ( containerEl ) . setName ( "Animation" ) . setHeading ( ) ;
2025-03-16 19:20:48 +00:00
2025-03-16 18:17:32 +00:00
// Add new setting for animation
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Show update animation" )
2025-03-16 19:20:48 +00:00
. setDesc ( "Show a brief animation when updating the progress bar" )
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . showUpdateAnimation )
. onChange ( async ( value ) = > {
this . plugin . settings . showUpdateAnimation = value ;
await this . plugin . saveSettings ( ) ;
// No need to update the view here as it will only affect future updates
} )
) ;
2025-03-16 18:17:32 +00:00
// Performance settings section
2025-04-15 14:38:12 +00:00
new Setting ( containerEl ) . setName ( "Performance" ) . setHeading ( ) ;
2025-03-16 19:20:48 +00:00
2025-03-16 18:17:32 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Editor change delay" )
2025-03-16 19:20:48 +00:00
. setDesc (
"Delay before updating after editor content changes (lower = more responsive, higher = better performance)"
)
. addSlider ( ( slider ) = >
slider
. setLimits ( 100 , 1000 , 50 ) // From 100ms to 1000ms in steps of 50ms
. setValue ( this . plugin . settings . editorChangeDelay )
. setDynamicTooltip ( )
. onChange ( async ( value ) = > {
this . plugin . settings . editorChangeDelay = value ;
await this . plugin . saveSettings ( ) ;
// Note: This will take effect after plugin reload
new Notice (
"Editor change delay updated. Restart plugin to apply changes."
) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default (500ms)" )
. onClick ( async ( ) = > {
this . plugin . settings . editorChangeDelay = 500 ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel to update the slider
new Notice (
"Editor change delay reset. Restart plugin to apply changes."
) ;
} )
) ;
2025-03-16 18:17:32 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Keyboard input delay" )
2025-03-16 19:20:48 +00:00
. setDesc (
"Delay after keyboard input before updating progress (in milliseconds)"
)
. addSlider ( ( slider ) = >
slider
. setLimits ( 100 , 1000 , 50 ) // From 0ms to 100ms in steps of 5ms
. setValue ( this . plugin . settings . keyboardInputDelay )
. setDynamicTooltip ( )
. onChange ( async ( value ) = > {
this . plugin . settings . keyboardInputDelay = value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default (100ms)" )
. onClick ( async ( ) = > {
this . plugin . settings . keyboardInputDelay = 100 ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel to update the slider
} )
) ;
2025-03-16 18:17:32 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Checkbox click delay" )
2025-03-16 19:20:48 +00:00
. setDesc (
"Delay after checkbox click before updating progress (in milliseconds)"
)
. addSlider ( ( slider ) = >
slider
. setLimits ( 100 , 1000 , 50 ) // From 10ms to 200ms in steps of 10ms
. setValue ( this . plugin . settings . checkboxClickDelay )
. setDynamicTooltip ( )
. onChange ( async ( value ) = > {
this . plugin . settings . checkboxClickDelay = value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default (200ms)" )
. onClick ( async ( ) = > {
this . plugin . settings . checkboxClickDelay = 200 ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel to update the slider
} )
) ;
2025-03-16 11:34:29 +00:00
// Add color scheme settings
2025-04-15 14:38:12 +00:00
new Setting ( containerEl ) . setName ( "Progress bar colors" ) . setHeading ( ) ;
2025-03-16 19:20:48 +00:00
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Color scheme" )
2025-03-16 19:20:48 +00:00
. setDesc ( "Choose a color scheme for the progress bar" )
. addDropdown ( ( dropdown ) = >
dropdown
. addOption ( "default" , "Default (Theme Colors)" )
. addOption ( "red-orange-green" , "Red-Orange-Blue-Green" )
. addOption ( "custom" , "Custom Colors" )
. setValue ( this . plugin . settings . progressColorScheme )
. onChange (
async (
value : "default" | "red-orange-green" | "custom"
) = > {
this . plugin . settings . progressColorScheme = value ;
// Set preset colors if red-orange-green is selected
if ( value === "red-orange-green" ) {
2025-03-20 11:32:16 +00:00
// Reset color values and thresholds
2025-03-16 19:20:48 +00:00
this . plugin . settings . lowProgressColor =
"#e06c75" ; // Red
this . plugin . settings . mediumProgressColor =
"#e5c07b" ; // Orange/Yellow
this . plugin . settings . highProgressColor =
"#61afef" ; // Blue
this . plugin . settings . completeProgressColor =
"#98c379" ; // Green
2025-03-20 11:32:16 +00:00
// Reset thresholds
2025-03-16 19:20:48 +00:00
this . plugin . settings . lowProgressThreshold = 30 ;
this . plugin . settings . mediumProgressThreshold = 60 ;
this . plugin . settings . highProgressThreshold = 99 ;
2025-03-20 11:32:16 +00:00
// Show notice to confirm changes
2025-03-16 19:20:48 +00:00
new Notice (
"Applied Red-Orange-Blue-Green color scheme"
) ;
}
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh to show/hide custom color options
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
}
)
) ;
2025-03-16 11:34:29 +00:00
// Only show custom color settings if custom is selected
2025-03-16 19:20:48 +00:00
if ( this . plugin . settings . progressColorScheme === "custom" ) {
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Low progress color" )
2025-03-16 19:20:48 +00:00
. setDesc (
` Color for progress below ${ this . plugin . settings . lowProgressThreshold } % `
)
. addText ( ( text ) = >
text
. setValue ( this . plugin . settings . lowProgressColor )
. onChange ( async ( value ) = > {
this . plugin . settings . lowProgressColor = value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Medium progress color" )
2025-03-16 19:20:48 +00:00
. setDesc (
` Color for progress between ${ this . plugin . settings . lowProgressThreshold } % and ${ this . plugin . settings . mediumProgressThreshold } % `
)
. addText ( ( text ) = >
text
. setValue ( this . plugin . settings . mediumProgressColor )
. onChange ( async ( value ) = > {
this . plugin . settings . mediumProgressColor = value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "High progress color" )
2025-03-16 19:20:48 +00:00
. setDesc (
` Color for progress between ${ this . plugin . settings . mediumProgressThreshold } % and ${ this . plugin . settings . highProgressThreshold } % `
)
. addText ( ( text ) = >
text
. setValue ( this . plugin . settings . highProgressColor )
. onChange ( async ( value ) = > {
this . plugin . settings . highProgressColor = value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Complete progress color" )
2025-03-16 19:20:48 +00:00
. setDesc ( "Color for 100% progress" )
. addText ( ( text ) = >
text
. setValue ( this . plugin . settings . completeProgressColor )
. onChange ( async ( value ) = > {
this . plugin . settings . completeProgressColor = value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
// Add threshold settings
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Low progress threshold" )
2025-03-16 19:20:48 +00:00
. setDesc ( "Percentage below which progress is considered low" )
. addSlider ( ( slider ) = >
slider
. setLimits ( 1 , 99 , 1 )
. setValue ( this . plugin . settings . lowProgressThreshold )
. setDynamicTooltip ( )
. onChange ( async ( value ) = > {
// Ensure thresholds don't overlap
if (
value >=
this . plugin . settings . mediumProgressThreshold
) {
value =
this . plugin . settings
. mediumProgressThreshold - 1 ;
}
this . plugin . settings . lowProgressThreshold = value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Medium progress threshold" )
2025-03-16 19:20:48 +00:00
. setDesc ( "Percentage below which progress is considered medium" )
. addSlider ( ( slider ) = >
slider
. setLimits ( 1 , 99 , 1 )
. setValue ( this . plugin . settings . mediumProgressThreshold )
. setDynamicTooltip ( )
. onChange ( async ( value ) = > {
// Ensure thresholds don't overlap
if (
value <=
this . plugin . settings . lowProgressThreshold
) {
value =
this . plugin . settings . lowProgressThreshold +
1 ;
}
if (
value >=
this . plugin . settings . highProgressThreshold
) {
value =
this . plugin . settings . highProgressThreshold -
1 ;
}
this . plugin . settings . mediumProgressThreshold =
value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "High progress threshold" )
2025-03-16 19:20:48 +00:00
. setDesc (
"Percentage below which progress is considered high (but not complete)"
)
. addSlider ( ( slider ) = >
slider
. setLimits ( 1 , 99 , 1 )
. setValue ( this . plugin . settings . highProgressThreshold )
. setDynamicTooltip ( )
. onChange ( async ( value ) = > {
// Ensure thresholds don't overlap
if (
value <=
this . plugin . settings . mediumProgressThreshold
) {
value =
this . plugin . settings
. mediumProgressThreshold + 1 ;
}
this . plugin . settings . highProgressThreshold = value ;
await this . plugin . saveSettings ( ) ;
// Update the view if open
const currentFile =
this . app . workspace . getActiveFile ( ) ;
if ( currentFile && this . plugin . sidebarView ) {
this . plugin . sidebarView . updateProgressBar (
currentFile
) ;
}
} )
) ;
2025-03-16 11:34:29 +00:00
}
2025-03-16 19:20:48 +00:00
// Interface settings section (Add this before or after Animation Settings)
2025-04-15 14:38:12 +00:00
new Setting ( containerEl ) . setName ( "Interface" ) . setHeading ( ) ;
2025-03-16 19:20:48 +00:00
// Fix the Max Tabs Height setting to allow proper input
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Max tabs height" )
2025-03-19 11:33:52 +00:00
. setDesc (
"Maximum height for workspace tabs (e.g., 110px, 200px, auto)"
)
2025-03-16 19:20:48 +00:00
. addText ( ( text ) = > {
// Set initial value
text . setValue ( this . plugin . settings . maxTabsHeight ) ;
2025-03-19 11:33:52 +00:00
2025-03-16 19:20:48 +00:00
// Apply improved validation only when the field loses focus
// This allows typing intermediary values that may not be valid yet
text . inputEl . addEventListener ( "blur" , async ( ) = > {
const value = text . inputEl . value ;
// Validate only when user is done editing
2025-03-19 11:33:52 +00:00
const isValid =
value === "auto" ||
value === "none" ||
2025-03-16 19:20:48 +00:00
/^\d+(\.\d+)?(px|em|rem|vh|%)$/ . test ( value ) ;
2025-03-19 11:33:52 +00:00
2025-03-16 19:20:48 +00:00
if ( isValid ) {
// Only update if changed to avoid unnecessary saves
if ( this . plugin . settings . maxTabsHeight !== value ) {
this . plugin . settings . maxTabsHeight = value ;
await this . plugin . saveSettings ( ) ;
new Notice ( ` Max tabs height updated to ${ value } ` ) ;
}
} else {
2025-03-19 11:33:52 +00:00
new Notice (
"Please enter 'auto', 'none' or a valid CSS length value (e.g., 110px)"
) ;
2025-03-16 19:20:48 +00:00
// Reset to previous valid value
text . setValue ( this . plugin . settings . maxTabsHeight ) ;
}
} ) ;
2025-03-19 11:33:52 +00:00
2025-03-16 19:20:48 +00:00
// Add Enter key handling
text . inputEl . addEventListener ( "keydown" , async ( event ) = > {
if ( event . key === "Enter" ) {
event . preventDefault ( ) ;
text . inputEl . blur ( ) ; // Trigger the blur event to validate
}
} ) ;
2025-03-19 11:33:52 +00:00
2025-03-16 19:20:48 +00:00
// Improve UX
text . inputEl . style . width = "120px" ;
text . inputEl . placeholder = "auto" ;
2025-03-19 11:33:52 +00:00
2025-03-16 19:20:48 +00:00
return text ;
} )
2025-03-19 11:33:52 +00:00
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default (auto)" )
. onClick ( async ( ) = > {
this . plugin . settings . maxTabsHeight = "auto" ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel
new Notice ( "Max tabs height reset to 'auto'" ) ;
} )
) ;
// Add Metadata Auto-Update Settings
2025-04-15 14:38:12 +00:00
new Setting ( containerEl ) . setName ( "Metadata auto-update" ) . setHeading ( ) ;
2025-03-19 11:33:52 +00:00
new Setting ( containerEl )
. setName ( "Auto-update metadata" )
. setDesc (
"Automatically update metadata when all tasks are completed"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . autoUpdateMetadata )
. onChange ( async ( value ) = > {
this . plugin . settings . autoUpdateMetadata = value ;
await this . plugin . saveSettings ( ) ;
// Refresh the display to show/hide related settings
this . display ( ) ;
} )
2025-03-16 19:20:48 +00:00
) ;
2025-03-19 11:33:52 +00:00
// Only show these settings if auto-update is enabled
if ( this . plugin . settings . autoUpdateMetadata ) {
new Setting ( containerEl )
. setName ( "Change status" )
. setDesc (
"Change 'status: In Progress' to 'status: Completed' when tasks reach 100%"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . autoChangeStatus )
. onChange ( async ( value ) = > {
this . plugin . settings . autoChangeStatus = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
new Setting ( containerEl )
. setName ( "Update finished date" )
. setDesc (
"Set 'finished: ' to today's date when tasks reach 100%"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . autoUpdateFinishedDate )
. onChange ( async ( value ) = > {
this . plugin . settings . autoUpdateFinishedDate = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
2025-03-19 12:25:02 +00:00
// Show status label settings if status changing is enabled
if ( this . plugin . settings . autoChangeStatus ) {
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Todo status label" )
2025-03-19 12:25:02 +00:00
. setDesc ( "Status label for files with 0% progress" )
. addText ( ( text ) = >
text
. setPlaceholder ( "Todo" )
. setValue ( this . plugin . settings . statusTodo )
. onChange ( async ( value ) = > {
this . plugin . settings . statusTodo = value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default" )
. onClick ( async ( ) = > {
this . plugin . settings . statusTodo = "Todo" ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ;
} )
) ;
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "In progress status label" )
2025-03-19 12:25:02 +00:00
. setDesc ( "Status label for files with 1-99% progress" )
. addText ( ( text ) = >
text
. setPlaceholder ( "In Progress" )
. setValue ( this . plugin . settings . statusInProgress )
. onChange ( async ( value ) = > {
this . plugin . settings . statusInProgress = value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default" )
. onClick ( async ( ) = > {
2025-03-20 07:21:00 +00:00
this . plugin . settings . statusInProgress =
"In Progress" ;
2025-03-19 12:25:02 +00:00
await this . plugin . saveSettings ( ) ;
this . display ( ) ;
} )
) ;
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Completed status label" )
2025-03-19 12:25:02 +00:00
. setDesc ( "Status label for files with 100% progress" )
. addText ( ( text ) = >
text
. setPlaceholder ( "Completed" )
. setValue ( this . plugin . settings . statusCompleted )
. onChange ( async ( value ) = > {
this . plugin . settings . statusCompleted = value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default" )
. onClick ( async ( ) = > {
2025-03-20 07:21:00 +00:00
this . plugin . settings . statusCompleted =
"Completed" ;
2025-03-19 12:25:02 +00:00
await this . plugin . saveSettings ( ) ;
this . display ( ) ;
} )
) ;
}
2025-03-20 07:21:00 +00:00
// Add Kanban integration settings
2025-04-15 14:38:12 +00:00
new Setting ( containerEl ) . setName ( "Kanban integration" ) . setHeading ( ) ;
2025-03-20 07:21:00 +00:00
new Setting ( containerEl )
. setName ( "Update Kanban boards" )
. setDesc (
"Automatically move cards in Kanban boards based on task status"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . autoUpdateKanban )
. onChange ( async ( value ) = > {
this . plugin . settings . autoUpdateKanban = value ;
await this . plugin . saveSettings ( ) ;
// Refresh to show/hide related settings
this . display ( ) ;
} )
) ;
if ( this . plugin . settings . autoUpdateKanban ) {
new Setting ( containerEl )
. setName ( "Sync Kanban columns with status" )
. setDesc (
"Match Kanban column names to status values (Todo, In Progress, Completed)"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . kanbanSyncWithStatus )
. onChange ( async ( value ) = > {
2025-03-20 09:43:08 +00:00
this . plugin . settings . kanbanSyncWithStatus =
value ;
2025-03-20 07:21:00 +00:00
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh settings
} )
) ;
// Only show this if sync with status is disabled (legacy mode)
if ( ! this . plugin . settings . kanbanSyncWithStatus ) {
new Setting ( containerEl )
2025-04-15 10:53:27 +00:00
. setName ( "Completed column name" )
2025-03-20 07:21:00 +00:00
. setDesc (
"The name of the column where completed items should be moved to (e.g., 'Complete', 'Done', 'Finished')"
)
. addText ( ( text ) = >
text
. setPlaceholder ( "Complete" )
. setValue (
this . plugin . settings . kanbanCompletedColumn
)
. onChange ( async ( value ) = > {
this . plugin . settings . kanbanCompletedColumn =
value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default (Complete)" )
. onClick ( async ( ) = > {
this . plugin . settings . kanbanCompletedColumn =
"Complete" ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ;
} )
) ;
}
// Add Auto-detect settings and other Kanban settings
new Setting ( containerEl )
. setName ( "Auto-detect Kanban boards" )
. setDesc (
"Automatically detect files that appear to be Kanban boards"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . kanbanAutoDetect )
. onChange ( async ( value ) = > {
this . plugin . settings . kanbanAutoDetect = value ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh settings
} )
) ;
// Note about column naming
const infoDiv = containerEl . createDiv ( {
cls : "kanban-info" ,
attr : {
2025-03-20 09:43:08 +00:00
style : "background: var(--background-secondary-alt); padding: 10px; border-radius: 5px; margin-top: 10px;" ,
} ,
2025-03-20 07:21:00 +00:00
} ) ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
infoDiv . createEl ( "p" , {
text : "ℹ ️ Column naming tip:" ,
attr : {
2025-03-20 09:43:08 +00:00
style : "font-weight: bold; margin: 0 0 5px 0;" ,
} ,
2025-03-20 07:21:00 +00:00
} ) ;
2025-03-20 09:43:08 +00:00
2025-03-20 07:21:00 +00:00
infoDiv . createEl ( "p" , {
text : ` To get the best results, name your Kanban columns to match the status values: " ${ this . plugin . settings . statusTodo } ", " ${ this . plugin . settings . statusInProgress } ", and " ${ this . plugin . settings . statusCompleted } ". ` ,
attr : {
2025-03-20 09:43:08 +00:00
style : "margin: 0;" ,
} ,
2025-03-20 07:21:00 +00:00
} ) ;
// Keep the remaining Kanban settings (specific files, exclude files, file picker)
// ...existing code...
}
2025-03-19 11:33:52 +00:00
}
2025-03-24 09:40:04 +00:00
// Add new section for auto-add to Kanban
new Setting ( containerEl )
. setName ( "Auto-add files to Kanban board" )
. setDesc (
"Automatically add files with tasks to a specified Kanban board if they're not already there"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . autoAddToKanban )
. onChange ( async ( value ) = > {
this . plugin . settings . autoAddToKanban = value ;
await this . plugin . saveSettings ( ) ;
// Refresh to show/hide related settings
this . display ( ) ;
} )
) ;
if ( this . plugin . settings . autoAddToKanban ) {
new Setting ( containerEl )
. setName ( "Target Kanban board" )
2025-04-15 11:25:00 +00:00
. setDesc (
"The path to the Kanban board where files should be added"
)
2025-03-24 09:40:04 +00:00
. addText ( ( text ) = >
text
. setPlaceholder ( "path/to/kanban.md" )
. setValue ( this . plugin . settings . autoAddKanbanBoard )
. onChange ( async ( value ) = > {
this . plugin . settings . autoAddKanbanBoard = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
// Add file picker button
containerEl . createEl ( "div" , {
text : "Select Kanban board file:" ,
2025-04-15 11:25:00 +00:00
attr : { style : "margin-left: 36px; margin-bottom: 8px;" } ,
2025-03-24 09:40:04 +00:00
} ) ;
const filePickerContainer = containerEl . createEl ( "div" , {
2025-04-15 11:25:00 +00:00
attr : { style : "margin-left: 36px; margin-bottom: 12px;" } ,
2025-03-24 09:40:04 +00:00
} ) ;
const filePickerButton = filePickerContainer . createEl ( "button" , {
text : "Browse..." ,
cls : "mod-cta" ,
} ) ;
filePickerButton . addEventListener ( "click" , async ( ) = > {
// Remove the problematic code that references app.plugins
try {
const modal = new FileSuggestModal ( this . app , this . plugin ) ;
modal . onChooseItem = ( file : TFile ) = > {
if ( file ) {
this . plugin . settings . autoAddKanbanBoard = file . path ;
this . plugin . saveSettings ( ) . then ( ( ) = > {
this . display ( ) ;
} ) ;
}
} ;
modal . open ( ) ;
} catch ( error ) {
2025-04-15 11:25:00 +00:00
new Notice (
"Error opening file picker. Please enter the path manually."
) ;
2025-03-24 09:40:04 +00:00
console . error ( "File picker error:" , error ) ;
}
} ) ;
new Setting ( containerEl )
. setName ( "Target column" )
2025-04-15 11:25:00 +00:00
. setDesc (
"The column where new files should be added (e.g., 'Todo', 'Backlog')"
)
2025-03-24 09:40:04 +00:00
. addText ( ( text ) = >
text
. setPlaceholder ( "Todo" )
. setValue ( this . plugin . settings . autoAddKanbanColumn )
. onChange ( async ( value ) = > {
this . plugin . settings . autoAddKanbanColumn = value ;
await this . plugin . saveSettings ( ) ;
} )
)
. addExtraButton ( ( button ) = >
button
. setIcon ( "reset" )
. setTooltip ( "Reset to default (Todo)" )
. onClick ( async ( ) = > {
this . plugin . settings . autoAddKanbanColumn = "Todo" ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ;
} )
) ;
2025-07-12 08:57:43 +00:00
}
// Add new section for custom checkbox states
new Setting ( containerEl )
. setName ( "Custom Checkbox States" )
2025-12-04 09:22:45 +00:00
. setDesc (
"Configure custom checkbox states for different Kanban columns"
)
2025-07-12 08:57:43 +00:00
. setHeading ( ) ;
new Setting ( containerEl )
. setName ( "Enable custom checkbox states" )
. setDesc (
"When enabled, cards will automatically update their checkbox states when moved between Kanban columns"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . enableCustomCheckboxStates )
. onChange ( async ( value ) = > {
this . plugin . settings . enableCustomCheckboxStates = value ;
await this . plugin . saveSettings ( ) ;
// Refresh to show/hide related settings
this . display ( ) ;
} )
) ;
if ( this . plugin . settings . enableCustomCheckboxStates ) {
// Add new setting for Kanban card checkbox sync
new Setting ( containerEl )
. setName ( "Enable Kanban card checkbox sync" )
. setDesc (
"When enabled, dragging cards between Kanban columns will automatically update checkbox states of the cards in the Kanban board"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . enableKanbanToFileSync )
. onChange ( async ( value ) = > {
this . plugin . settings . enableKanbanToFileSync = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
// Add new setting for auto-sync on Kanban open
new Setting ( containerEl )
. setName ( "Auto-sync checkbox states on Kanban open" )
. setDesc (
"When enabled, automatically sync all card checkbox states to match their columns when opening a Kanban board (runs once per session for performance)"
)
. addToggle ( ( toggle ) = >
toggle
. setValue ( this . plugin . settings . enableKanbanAutoSync )
. onChange ( async ( value ) = > {
this . plugin . settings . enableKanbanAutoSync = value ;
await this . plugin . saveSettings ( ) ;
} )
) ;
// NEW: Add setting for Kanban normalization protection
new Setting ( containerEl )
2025-12-04 09:22:45 +00:00
. setName (
"Protect custom checkbox states from Kanban normalization"
)
2025-07-12 08:57:43 +00:00
. setDesc (
"When enabled, prevents the Kanban plugin from automatically converting custom checkbox states (like [/], [~]) to standard states ([x]). This preserves your custom state mappings."
)
. addToggle ( ( toggle ) = >
toggle
2025-12-04 09:22:45 +00:00
. setValue (
this . plugin . settings
. enableKanbanNormalizationProtection
)
2025-07-12 08:57:43 +00:00
. onChange ( async ( value ) = > {
2025-12-04 09:22:45 +00:00
this . plugin . settings . enableKanbanNormalizationProtection =
value ;
2025-07-12 08:57:43 +00:00
await this . plugin . saveSettings ( ) ;
} )
) ;
// Add explanation
const infoDiv = containerEl . createDiv ( {
cls : "custom-checkbox-info" ,
attr : {
style : "background: var(--background-secondary-alt); padding: 10px; border-radius: 5px; margin: 10px 0;" ,
} ,
} ) ;
infoDiv . createEl ( "p" , {
text : "ℹ ️ Custom Checkbox States Configuration:" ,
attr : {
style : "font-weight: bold; margin: 0 0 5px 0;" ,
} ,
} ) ;
infoDiv . createEl ( "p" , {
text : "Define which checkbox state should be used for each Kanban column. Common states include: [ ] (todo), [/] (in progress), [x] (completed), [>] (forwarded), [-] (cancelled)." ,
attr : {
style : "margin: 0 0 5px 0;" ,
} ,
} ) ;
// Display current mappings
2025-12-04 09:22:45 +00:00
this . plugin . settings . kanbanColumnCheckboxMappings . forEach (
( mapping , index ) = > {
const mappingContainer = containerEl . createDiv ( {
cls : "checkbox-mapping-container" ,
attr : {
style : "display: flex; gap: 10px; align-items: center; margin: 10px 0; padding: 10px; border: 1px solid var(--background-modifier-border); border-radius: 5px;" ,
} ,
} ) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
// Column name input
const columnInput = mappingContainer . createEl ( "input" , {
type : "text" ,
value : mapping.columnName ,
attr : {
placeholder : "Column Name" ,
style : "flex: 1; padding: 5px;" ,
} ,
} ) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
// Checkbox state input
const checkboxInput = mappingContainer . createEl ( "input" , {
type : "text" ,
value : mapping.checkboxState ,
attr : {
placeholder : "[ ]" ,
style : "width: 60px; padding: 5px; text-align: center;" ,
} ,
} ) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
// Delete button
const deleteButton = mappingContainer . createEl ( "button" , {
text : "Remove" ,
cls : "mod-warning" ,
attr : {
style : "padding: 5px 10px;" ,
} ,
} ) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
// Event listeners
columnInput . addEventListener ( "change" , async ( ) = > {
this . plugin . settings . kanbanColumnCheckboxMappings [
index
] . columnName = columnInput . value ;
await this . plugin . saveSettings ( ) ;
} ) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
checkboxInput . addEventListener ( "change" , async ( ) = > {
this . plugin . settings . kanbanColumnCheckboxMappings [
index
] . checkboxState = checkboxInput . value ;
await this . plugin . saveSettings ( ) ;
} ) ;
2025-07-12 08:57:43 +00:00
2025-12-04 09:22:45 +00:00
deleteButton . addEventListener ( "click" , async ( ) = > {
this . plugin . settings . kanbanColumnCheckboxMappings . splice (
index ,
1
) ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel
} ) ;
}
) ;
2025-07-12 08:57:43 +00:00
// Add new mapping button
const addMappingButton = containerEl . createEl ( "button" , {
text : "Add Column Mapping" ,
cls : "mod-cta" ,
attr : {
style : "margin: 10px 0;" ,
} ,
} ) ;
addMappingButton . addEventListener ( "click" , async ( ) = > {
this . plugin . settings . kanbanColumnCheckboxMappings . push ( {
columnName : "" ,
checkboxState : "[ ]" ,
} ) ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel
} ) ;
// Reset to defaults button
const resetButton = containerEl . createEl ( "button" , {
text : "Reset to Defaults" ,
cls : "mod-warning" ,
attr : {
style : "margin: 10px 0;" ,
} ,
} ) ;
resetButton . addEventListener ( "click" , async ( ) = > {
this . plugin . settings . kanbanColumnCheckboxMappings = [
{ columnName : "Todo" , checkboxState : "[ ]" } ,
{ columnName : "In Progress" , checkboxState : "[/]" } ,
{ columnName : "Complete" , checkboxState : "[x]" } ,
{ columnName : "Done" , checkboxState : "[x]" } ,
] ;
await this . plugin . saveSettings ( ) ;
this . display ( ) ; // Refresh the settings panel
} ) ;
2025-03-24 09:40:04 +00:00
}
2022-04-15 18:13:31 +00:00
}
}
2025-03-24 09:40:04 +00:00
// Helper class for file picking
class FileSuggestModal extends SuggestModal < TFile > {
2025-04-15 11:25:00 +00:00
plugin : TaskProgressBarPlugin ;
onChooseItem : ( file : TFile ) = > void ;
constructor ( app : App , plugin : TaskProgressBarPlugin ) {
super ( app ) ;
this . plugin = plugin ;
this . onChooseItem = ( ) = > { } ; // Default empty implementation
}
getSuggestions ( query : string ) : TFile [ ] {
const files = this . app . vault . getMarkdownFiles ( ) ;
// Filter to only show potential Kanban board files
const kanbanFiles = files . filter ( ( file ) = > {
// Show all Markdown files when no query
if ( ! query ) return true ;
// Otherwise filter by name/path containing the query
return file . path . toLowerCase ( ) . includes ( query . toLowerCase ( ) ) ;
} ) ;
return kanbanFiles ;
}
renderSuggestion ( file : TFile , el : HTMLElement ) {
el . createEl ( "div" , { text : file.path } ) ;
}
// Implement the required abstract method
onChooseSuggestion ( file : TFile , evt : MouseEvent | KeyboardEvent ) {
if ( this . onChooseItem ) {
this . onChooseItem ( file ) ;
}
}
2025-03-24 09:40:04 +00:00
}