diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9cc1eae0..ba29f2a8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -95,7 +95,7 @@ npm run dev # Should start watch mode and display "watching for changes..." │ │ ├── Column.tsx # Kanban column component │ │ ├── TaskItem.tsx # Individual task component (26k lines - complex) │ │ ├── KanbanBoard.tsx # Main board component -│ │ ├── TaskBoardViewContent.tsx # Main board content wrapper +│ │ ├── TaskBoardViewContainer.tsx # Main board content wrapper │ │ └── MapView.tsx # Map-based view component │ ├── interfaces/ # TypeScript interfaces (3 files) │ │ ├── TaskItem.ts # Task data structures diff --git a/README.md b/README.md index e866b6c2..d27ff79b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

An Obsidian plugin to view and manage all your tasks, throughout the vault on a centralized board using various kinds of views. Use boards to manage small to large projects.

-![Task Board Thumbnail](./assets/MainThumbnail-3.jpg) +![Task Board Thumbnail](./assets/MainThumbnail-4.png) > YouTube playlist : [Task Board - Obsidian plugin](https://youtube.com/playlist?list=PLqEPxsDi1dtepfcaUO9r1BTGZN6IuhvvH&si=yohu1rczpRVq68D6) @@ -51,7 +51,7 @@ Join the forum top to share your thoughts, ideas or requests and hear from other **Step 3 :** Click on the **Scan vault modal** button from the top-right corner in the Task Board view header. Then click on the run button and it will scan all your files to look for tasks. If your vault contains thousands of notes, you can apply [scanning filters](https://tu2-atmanand.github.io/task-board-docs/docs/Features/Filters_for_Scanning/) to exclude certain files from scanning. -**Step 4 :** There are already three predefined board for your convenience as an example. Feel free to delete or edit the boards and [create your own boards](https://tu2-atmanand.github.io/task-board-docs/docs/How_To/HowToCreateNewBoard/) from the [Board configure modal](https://tu2-atmanand.github.io/task-board-docs/docs/How_To/HowToUseBoardSettings/). +**Step 4 :** There are already three predefined board for your convenience as an example. Feel free to delete or edit the boards and [create your own boards](https://tu2-atmanand.github.io/task-board-docs/docs/How_To/HowToCreateNewBoard/) from the [Board configure modal](https://tu2-atmanand.github.io/task-board-docs/docs/How_To/HowToUseBoardSettings/). Enjoy ! @@ -124,11 +124,11 @@ This advanced and dynamic filters will help you to use boards as separate projec You can contribute to this project by : -**1. Requesting a new feature, suggesting an improvement or reporting a Bug :** [How to create a new request](https://tu2-atmanand.github.io/task-board-docs/Advanced/HowToCreateRequest.html). +**1. Requesting a new feature, suggesting an improvement or reporting a Bug :** [How to create a new request](https://tu2-atmanand.github.io/task-board-docs/docs/Advanced/HowToCreateRequest). -**2. Improving the translated languages or add a new language :** [How to Contribute for Language Translation](https://tu2-atmanand.github.io/task-board-docs/Advanced/Contribution_For_Languages.html). +**2. Improving the translated languages or add a new language :** [How to Contribute for Language Translation](https://tu2-atmanand.github.io/task-board-docs/docs/Advanced/Contribution_For_Languages). -**3. Contribute to the Development of the plugin Code :** : [How to join the plugin development](https://tu2-atmanand.github.io/task-board-docs/Advanced/HowToJoinDevelopment.html). +**3. Contribute to the Development of the plugin Code :** : [How to join the plugin development](https://tu2-atmanand.github.io/task-board-docs/docs/Advanced/HowToJoinDevelopment). ## Motivation for the Project diff --git a/assets/MainThumbnail-3.jpg b/assets/MainThumbnail-3.jpg deleted file mode 100644 index cff0c156..00000000 Binary files a/assets/MainThumbnail-3.jpg and /dev/null differ diff --git a/assets/MainThumbnail-4.png b/assets/MainThumbnail-4.png new file mode 100644 index 00000000..fdffde10 Binary files /dev/null and b/assets/MainThumbnail-4.png differ diff --git a/backup-data.json b/backup-data.json new file mode 100644 index 00000000..9dc12541 --- /dev/null +++ b/backup-data.json @@ -0,0 +1,291 @@ +{ + "version": "2.0.0-beta-2", + "data": { + "lang": "en", + "openOnStartup": false, + "scanFilters": { + "files": { + "polarity": 3, + "values": [] + }, + "folders": { + "polarity": 3, + "values": [] + }, + "frontmatter": { + "polarity": 3, + "values": [] + }, + "tags": { + "polarity": 3, + "values": [] + } + }, + "firstDayOfWeek": "Mon", + "showTaskWithoutMetadata": true, + "ignoreFileNameDates": false, + "taskPropertyFormat": "2", + "dailyNotesPluginComp": false, + "dateFormat": "yyyy-MM-dd", + "dateTimeFormat": "yyyy-MM-dd'T'HH:mm:ss", + "defaultStartTime": "", + "taskCompletionInLocalTime": true, + "taskCompletionShowUtcOffset": false, + "autoAddUniversalDate": true, + "autoAddCreatedDate": false, + "autoAddCompletedDate": false, + "autoAddCancelledDate": false, + "showModifiedFilesNotice": true, + "scanMode": "automatic", + "columnWidth": "300px", + "visiblePropertiesList": [ + "title", + "tags", + "time", + "reminder", + "createdDate", + "startDate", + "scheduledDate", + "dueDate", + "completionDate", + "cancelledDate", + "dependsOn", + "status", + "id" + ], + "taskCardStyle": "emoji", + "showVerticalScroll": false, + "tagColors": [ + { + "name": "bug", + "color": "rgba(255, 0, 0, 0.55)", + "priority": 1 + }, + { + "name": "important", + "color": "rgba(246, 255, 0, 0.53)", + "priority": 2 + }, + { + "name": "wip", + "color": "rgba(0, 255, 0, 0.53)", + "priority": 2 + }, + { + "name": "review", + "color": "rgba(0, 0, 255, 0.49)", + "priority": 3 + } + ], + "editButtonAction": "popUp", + "doubleClickCardToEdit": "none", + "universalDate": "due", + "tagColorsType": "tagBg", + "customStatuses": [ + { + "symbol": " ", + "name": "Todo", + "nextStatusSymbol": "x", + "availableAsCommand": false, + "type": "TODO" + }, + { + "symbol": "<", + "name": "Ready to start", + "nextStatusSymbol": "x", + "availableAsCommand": false, + "type": "TODO" + }, + { + "symbol": "?", + "name": "In Review", + "nextStatusSymbol": "x", + "availableAsCommand": false, + "type": "TODO" + }, + { + "symbol": "/", + "name": "In Progress", + "nextStatusSymbol": "x", + "availableAsCommand": true, + "type": "IN_PROGRESS" + }, + { + "symbol": "x", + "name": "Done", + "nextStatusSymbol": " ", + "availableAsCommand": true, + "type": "IN_PROGRESS" + }, + { + "symbol": "X", + "name": "Completed", + "nextStatusSymbol": " ", + "availableAsCommand": true, + "type": "IN_PROGRESS" + }, + { + "symbol": "-", + "name": "Cancelled", + "nextStatusSymbol": "x", + "availableAsCommand": true, + "type": "CANCELLED" + }, + { + "symbol": "*", + "name": "Now", + "nextStatusSymbol": "r", + "availableAsCommand": false, + "type": "IN_PROGRESS" + } + ], + "compatiblePlugins": { + "dailyNotesPlugin": false, + "dayPlannerPlugin": false, + "tasksPlugin": false, + "reminderPlugin": false, + "quickAddPlugin": false + }, + "taskNoteIdentifierTag": "taskNote", + "preDefinedNote": "Meta/Task_Board/New_Tasks.md", + "archivedTasksFilePath": "", + "taskNoteDefaultLocation": "Meta/Task_Board/Task_Notes", + "archivedTBNotesFolderPath": "Meta/Task_Board/Archived_Task_Notes", + "quickAddPluginDefaultChoice": "", + "frontmatterFormatting": [ + { + "index": 0, + "property": "ID", + "key": "id", + "taskItemKey": "id" + }, + { + "index": 1, + "property": "Title", + "key": "title", + "taskItemKey": "title" + }, + { + "index": 2, + "property": "Status", + "key": "status", + "taskItemKey": "status" + }, + { + "index": 3, + "property": "Priority", + "key": "priority", + "taskItemKey": "priority" + }, + { + "index": 4, + "property": "Tags", + "key": "tags", + "taskItemKey": "tags" + }, + { + "index": 5, + "property": "Time", + "key": "time", + "taskItemKey": "time" + }, + { + "index": 6, + "property": "Reminder", + "key": "reminder", + "taskItemKey": "reminder" + }, + { + "index": 7, + "property": "Created date", + "key": "created-date", + "taskItemKey": "createdDate" + }, + { + "index": 8, + "property": "Start date", + "key": "start-date", + "taskItemKey": "startDate" + }, + { + "index": 9, + "property": "Scheduled date", + "key": "scheduled-date", + "taskItemKey": "scheduledDate" + }, + { + "index": 10, + "property": "Due date", + "key": "due-date", + "taskItemKey": "due" + }, + { + "index": 11, + "property": "Depends on", + "key": "depends-on", + "taskItemKey": "dependsOn" + }, + { + "index": 12, + "property": "Completed date", + "key": "cancelled-date", + "taskItemKey": "cancelledDate" + }, + { + "index": 13, + "key": "completed-date", + "taskItemKey": "completionDate" + } + ], + "showFrontmatterTagsOnCards": false, + "tasksCacheFilePath": "", + "notificationService": "none", + "actions": [ + { + "enabled": true, + "trigger": "Complete", + "type": "move", + "targetColumn": "Completed" + } + ], + "hiddenTaskProperties": [], + "autoAddUniqueID": false, + "uniqueIdCounter": 670, + "experimentalFeatures": false, + "safeGuardFeature": true, + "lastViewHistory": { + "boardFilePath": "Meta/Task_Board/Boards/Time Based Workflow.taskboard", + "settingTab": 2, + "taskId": "" + }, + "boundTaskCompletionToChildTasks": false, + "mapView": { + "background": "none", + "mapOrientation": "hor", + "optimizedRender": false, + "arrowDirection": "c2p", + "animatedEdges": false, + "scrollAction": "zoom", + "showMinimap": true, + "renderVisibleNodes": false, + "edgeType": "default" + }, + "taskBoardFilesRegistry": { + "901052398": { + "boardId": "901052398", + "filePath": "TaskBoard-Template-1774097758616.taskboard", + "boardName": "My Project", + "boardDescription": "This is my personal project. This is a default board created by Task Board for you to kick start your journey with Task Board. Feel free to edit or create new boards." + }, + "2908513791": { + "boardId": "2908513791", + "filePath": "My Project Board.taskboard", + "boardName": "My Project", + "boardDescription": "This is my personal project. This is a default board created by Task Board for you to kick start your journey with Task Board. Feel free to edit or create new boards." + } + }, + "loadAllBoards": false, + "searchQuery": "", + "dragAutoScrollEdgePercent": 20 + } +} \ No newline at end of file diff --git a/data.json b/data.json index c9c0770c..dd92d18e 100644 --- a/data.json +++ b/data.json @@ -1,1193 +1,334 @@ { - "version": "1.8.7", + "version": "2.0.0-beta-2", "data": { - "boardConfigs": [ + "lang": "en", + "openOnStartup": false, + "scanFilters": { + "files": { + "polarity": 3, + "values": [ + "A new file added.md", + "Task_board_note.md" + ] + }, + "folders": { + "polarity": 3, + "values": [ + "Daily_Notes" + ] + }, + "frontmatter": { + "polarity": 3, + "values": [ + "[\"publish\": true]" + ] + }, + "tags": { + "polarity": 3, + "values": [ + "#placeholder" + ] + } + }, + "firstDayOfWeek": "Mon", + "showTaskWithoutMetadata": true, + "ignoreFileNameDates": false, + "taskPropertyFormat": "2", + "dateFormat": "yyyy-MM-dd", + "dateTimeFormat": "yyyy-MM-dd'T'HH:mm:ss", + "dailyNotesPluginComp": false, + "defaultStartTime": "", + "taskCompletionInLocalTime": true, + "taskCompletionShowUtcOffset": false, + "autoAddUniversalDate": true, + "autoAddCreatedDate": true, + "autoAddCompletedDate": true, + "autoAddCancelledDate": false, + "showModifiedFilesNotice": true, + "scanMode": "automatic", + "columnWidth": "340px", + "visiblePropertiesList": [ + "checkbox", + "id", + "title", + "subTasksMinimized", + "descriptionMinimized", + "status", + "tags", + "time", + "reminder", + "priority", + "createdDate", + "startDate", + "scheduledDate", + "dueDate", + "completionDate", + "cancelledDate", + "dependsOn", + "filePath" + ], + "taskCardStyle": "bases", + "showVerticalScroll": false, + "dragAutoScrollEdgePercent": 20, + "tagColors": [ + { + "name": "bug", + "color": "rgba(167, 6, 6, 0.71)", + "priority": 1 + }, + { + "name": "important", + "color": "rgba(161, 167, 9, 0.79)", + "priority": 2 + }, + { + "name": "wip", + "color": "rgba(8, 149, 8, 0.78)", + "priority": 2 + }, + { + "name": "review", + "color": "rgba(22, 22, 181, 0.84)", + "priority": 3 + }, + { + "name": "feat", + "color": "rgba(69, 15, 183, 1)", + "priority": 5 + } + ], + "editButtonAction": "popUp", + "doubleClickCardToEdit": "noteInTab", + "universalDate": "due", + "tagColorsType": "tagBg", + "customStatuses": [ + { + "symbol": " ", + "name": "Todo", + "nextStatusSymbol": "x", + "availableAsCommand": false, + "type": "TODO" + }, + { + "symbol": "<", + "name": "Ready to start", + "nextStatusSymbol": "x", + "availableAsCommand": false, + "type": "TODO" + }, + { + "symbol": "?", + "name": "In Review", + "nextStatusSymbol": "x", + "availableAsCommand": false, + "type": "TODO" + }, + { + "symbol": "/", + "name": "In Progress", + "nextStatusSymbol": "x", + "availableAsCommand": true, + "type": "IN_PROGRESS" + }, { - "name": "Path filtered", - "index": 0, - "columns": [ - { - "id": 3061361157, - "index": 1, - "colType": "pathFiltered", - "active": true, - "collapsed": false, - "name": "TaskNotes", - "filePaths": "TaskNotes/", - "sortCriteria": [ - { - "criteria": "manualOrder", - "order": "asc", - "priority": 1, - "uid": "ur7dguyt" - } - ], - "tasksIdManualOrder": [ - "367", - "372", - "398696308", - "373", - "370", - "3077781059", - "4184100420", - "3052793295", - "1506909345", - 274, - "4031700447", - "362" - ] - }, - { - "id": 1312742179, - "index": 2, - "colType": "pathFiltered", - "active": true, - "collapsed": false, - "name": "Indented Bug", - "filePaths": "Testing Indented Task Bug.md", - "filters": { - "rootCondition": "any", - "filterGroups": [] - } - }, - { - "id": 2025781698, - "index": 3, - "colType": "pathFiltered", - "active": true, - "collapsed": false, - "name": "QMD", - "filePaths": "A QMD file.qmd" - }, - { - "id": 1623496994, - "index": 4, - "colType": "completed", - "active": true, - "collapsed": false, - "name": "Completed", - "limit": 20 - } - ], - "hideEmptyColumns": false, - "boardFilter": { - "rootCondition": "any", - "filterGroups": [] - }, - "taskCount": { - "pending": 65, - "completed": 19 - } + "symbol": "x", + "name": "Done", + "nextStatusSymbol": " ", + "availableAsCommand": true, + "type": "DONE" }, { - "columns": [ - { - "id": 1799377278, - "index": 1, - "colType": "undated", - "active": true, - "collapsed": false, - "name": "Backlog", - "datedBasedColumn": { - "from": 0, - "to": 0, - "dateType": "scheduledDate" - }, - "filters": { - "rootCondition": "any", - "filterGroups": [] - } - }, - { - "id": 3492, - "colType": "dated", - "active": true, - "collapsed": false, - "name": "Over Due", - "index": 2, - "datedBasedColumn": { - "dateType": "scheduledDate", - "from": -300, - "to": -1 - }, - "sortCriteria": [ - { - "criteria": "scheduledDate", - "order": "asc", - "priority": 1 - }, - { - "criteria": "time", - "order": "asc", - "priority": 2 - } - ], - "filters": { - "rootCondition": "any", - "filterGroups": [] - } - }, - { - "id": 3494, - "colType": "dated", - "active": true, - "collapsed": false, - "name": "Today", - "index": 3, - "datedBasedColumn": { - "dateType": "scheduledDate", - "from": 0, - "to": 0 - }, - "sortCriteria": [ - { - "criteria": "time", - "order": "asc", - "priority": 1 - } - ], - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "minimized": true - }, - { - "id": 3493, - "colType": "dated", - "active": true, - "collapsed": false, - "name": "Tomorrow", - "index": 4, - "datedBasedColumn": { - "dateType": "scheduledDate", - "from": 1, - "to": 1 - } - }, - { - "id": 3495, - "colType": "dated", - "active": true, - "collapsed": false, - "name": "Future", - "index": 5, - "datedBasedColumn": { - "dateType": "scheduledDate", - "from": 2, - "to": 300 - } - }, - { - "id": 3497, - "colType": "completed", - "active": true, - "collapsed": false, - "limit": 20, - "name": "Completed, updated", - "index": 6, - "filters": { - "rootCondition": "any", - "filterGroups": [] - } - } - ], - "filters": [ - "#Test", - "#working", - "#new" - ], - "filterPolarity": "0", - "filterScope": "Both", - "name": "Time based workflow", + "symbol": "X", + "name": "Completed", + "nextStatusSymbol": " ", + "availableAsCommand": true, + "type": "DONE" + }, + { + "symbol": "-", + "name": "Cancelled", + "nextStatusSymbol": "x", + "availableAsCommand": true, + "type": "CANCELLED" + } + ], + "compatiblePlugins": { + "dailyNotesPlugin": false, + "dayPlannerPlugin": false, + "tasksPlugin": false, + "reminderPlugin": false, + "quickAddPlugin": false + }, + "taskNoteIdentifierTag": "taskNote", + "preDefinedNote": "Meta/Task_Board/New_Tasks.md", + "archivedTasksFilePath": "", + "taskNoteDefaultLocation": "Meta/Task_Board/Task_Notes", + "archivedTBNotesFolderPath": "Meta/Task_Board/Archived_Task_Notes", + "quickAddPluginDefaultChoice": "", + "frontmatterFormatting": [ + { "index": 1, - "showColumnTags": true, - "showFilteredTags": false, - "hideEmptyColumns": false, - "boardFilter": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1763871967163-8a21jnuiq", - "groupCondition": "any", - "filters": [ - { - "id": "id-1763871967165-lct3l84v6", - "property": "tags", - "condition": "doesNotContain", - "value": "#task" - }, - { - "id": "id-1763871980225-nbzhu6dnv", - "property": "tags", - "condition": "doesNotContain", - "value": "#taskNote" - } - ] - } - ] - }, - "description": "Project to manage all the tasks related to plugin release.", - "taskCount": { - "pending": 4, - "completed": 1 - } + "property": "ID", + "key": "id", + "taskItemKey": "id" }, { - "columns": [ - { - "id": 3187486162, - "index": 1, - "colType": "otherTags", - "active": true, - "collapsed": false, - "name": "Other tags", - "minimized": false - }, - { - "colType": "untagged", - "active": true, - "collapsed": false, - "name": "Backlogs", - "index": 2, - "id": 226119, - "sortCriterias": [], - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "sortCriteria": [ - { - "criteria": "id", - "order": "desc", - "priority": 1, - "uid": "k6wjlc63" - } - ], - "minimized": false - }, - { - "id": 2485661779, - "index": 3, - "colType": "namedTag", - "active": false, - "collapsed": false, - "name": "#Task", - "coltag": "#Task", - "sortCriteria": [ - { - "criteria": "content", - "order": "asc", - "priority": 1, - "uid": "bmlpsogj" - } - ], - "tasksIdManualOrder": [ - "365", - 274, - "362" - ], - "minimized": false, - "filters": { - "rootCondition": "any", - "filterGroups": [] - } - }, - { - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "Can be implemented", - "index": 4, - "coltag": "#pending", - "id": 47626, - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "minimized": false, - "sortCriteria": [ - { - "criteria": "manualOrder", - "order": "asc", - "priority": 1, - "uid": "285zohor" - } - ], - "tasksIdManualOrder": [ - "224", - "276", - "389", - "378", - "355", - "258", - "375" - ] - }, - { - "id": 3130768414, - "index": 5, - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "In Progress", - "coltag": "wip" - }, - { - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "Ready to publish", - "index": 6, - "coltag": "done", - "id": 328227, - "minimized": false - }, - { - "id": 1936828579, - "index": 7, - "colType": "namedTag", - "active": false, - "collapsed": false, - "name": "*/seeding/*", - "coltag": "*/seeding/*", - "minimized": false - }, - { - "id": 3193261849, - "index": 8, - "colType": "completed", - "active": true, - "collapsed": false, - "name": "Completed", - "limit": 20, - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "minimized": false - } - ], - "filters": [ - "#Test", - "#working", - "*/seeding/*" - ], - "filterPolarity": "0", - "filterScope": "Both", - "name": "Tag based board", "index": 2, - "showColumnTags": true, - "showFilteredTags": true, - "hideEmptyColumns": false, - "boardFilter": { - "rootCondition": "all", - "filterGroups": [] - }, - "filterConfig": { - "enableSavedFilters": true, - "savedConfigs": [ - { - "id": "filter-config-1760103432145-fevybeh8j", - "name": "Destinatio", - "description": "Filtering all tasks from \"Destinatio\" folder", - "filterState": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1760029960520-z2a1g0ka4", - "groupCondition": "all", - "filters": [ - { - "id": "id-1760029960521-t33fwt542", - "property": "filePath", - "condition": "startsWith", - "value": "Destinatio" - } - ] - } - ] - }, - "createdAt": "2025-10-10T13:37:12.145Z", - "updatedAt": "2025-10-10T13:37:12.145Z" - }, - { - "id": "filter-config-1764176875594-8gv5ee69p", - "name": "This", - "description": "If \"This\" is present in the content.", - "filterState": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1760365709344-buyyxwhhh", - "groupCondition": "all", - "filters": [ - { - "id": "id-1760365709345-8uyz54hm3", - "property": "content", - "condition": "contains", - "value": "This" - } - ] - } - ] - }, - "createdAt": "2025-11-26T17:07:55.594Z", - "updatedAt": "2025-11-26T17:07:55.594Z" - }, - { - "id": "filter-config-1764176933714-tm6ozdue7", - "name": "This and No", - "description": "Content contains this and no", - "filterState": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1760029960520-z2a1g0ka4", - "groupCondition": "all", - "filters": [ - { - "id": "id-1760029960521-t33fwt542", - "property": "content", - "condition": "contains", - "value": "This" - } - ] - }, - { - "id": "id-1764176913802-bwvk75kr4", - "groupCondition": "all", - "filters": [ - { - "id": "id-1764176913803-1ugczxozs", - "property": "content", - "condition": "contains", - "value": "No" - } - ] - } - ] - }, - "createdAt": "2025-11-26T17:08:53.714Z", - "updatedAt": "2025-11-26T17:08:53.714Z" - }, - { - "id": "filter-config-1764177013691-5uj7krjmm", - "name": "This and No, both", - "description": "Both", - "filterState": { - "rootCondition": "all", - "filterGroups": [ - { - "id": "id-1760029960520-z2a1g0ka4", - "groupCondition": "all", - "filters": [ - { - "id": "id-1760029960521-t33fwt542", - "property": "content", - "condition": "contains", - "value": "This" - } - ] - }, - { - "id": "id-1764176913802-bwvk75kr4", - "groupCondition": "all", - "filters": [ - { - "id": "id-1764176913803-1ugczxozs", - "property": "content", - "condition": "contains", - "value": "No" - } - ] - } - ] - }, - "createdAt": "2025-11-26T17:10:13.691Z", - "updatedAt": "2025-11-26T17:10:13.691Z" - } - ] - }, - "taskCount": { - "pending": 63, - "completed": 14 - }, - "swimlanes": { - "enabled": true, - "hideEmptySwimlanes": false, - "property": "tags", - "maxHeight": "500px", - "sortCriteria": "custom", - "customSortOrder": [ - { - "value": "first", - "index": 1 - }, - { - "value": "second", - "index": 2 - }, - { - "value": "third", - "index": 3 - } - ], - "groupAllRest": true, - "verticalHeaderUI": true, - "minimized": [] - } + "property": "Title", + "key": "title", + "taskItemKey": "title" }, { - "columns": [ - { - "colType": "untagged", - "active": true, - "collapsed": false, - "name": "Backlogs", - "index": 1, - "id": 226119, - "sortCriterias": [], - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "sortCriteria": [ - { - "criteria": "content", - "order": "desc", - "priority": 1, - "uid": "k6wjlc63" - } - ], - "minimized": false - }, - { - "id": 2485661779, - "index": 2, - "colType": "namedTag", - "active": false, - "collapsed": false, - "name": "#Task", - "coltag": "#Task", - "sortCriteria": [ - { - "criteria": "content", - "order": "asc", - "priority": 1, - "uid": "bmlpsogj" - } - ], - "tasksIdManualOrder": [ - "365", - 274, - "362" - ], - "minimized": false, - "filters": { - "rootCondition": "any", - "filterGroups": [] - } - }, - { - "id": 1936828579, - "index": 3, - "colType": "namedTag", - "active": false, - "collapsed": false, - "name": "*/seeding/*", - "coltag": "*/seeding/*" - }, - { - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "Can be implemented", - "index": 4, - "coltag": "#pending", - "id": 47626, - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "minimized": false, - "sortCriteria": [ - { - "criteria": "manualOrder", - "order": "asc", - "priority": 1, - "uid": "pn3wvwal" - } - ], - "tasksIdManualOrder": [ - "375", - "389", - "224", - "355", - "276", - "258", - "378" - ] - }, - { - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "In Progress", - "index": 5, - "coltag": "working", - "id": 908753, - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "minimized": false, - "workLimit": 5 - }, - { - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "In Review", - "index": 6, - "coltag": "Test", - "id": 396902, - "minimized": false, - "workLimit": 3 - }, - { - "colType": "namedTag", - "active": true, - "collapsed": false, - "name": "Ready to publish", - "index": 7, - "coltag": "done", - "id": 328227, - "minimized": false - }, - { - "id": 3193261849, - "index": 8, - "colType": "completed", - "active": true, - "collapsed": false, - "name": "Completed", - "limit": 20, - "filters": { - "rootCondition": "any", - "filterGroups": [] - }, - "minimized": false - } - ], - "filters": [ - "#Test", - "#working", - "*/seeding/*" - ], - "filterPolarity": "0", - "filterScope": "Both", - "name": "Tag based board (copy)", "index": 3, - "showColumnTags": true, - "showFilteredTags": true, - "hideEmptyColumns": false, - "boardFilter": { - "rootCondition": "any", - "filterGroups": [] - }, - "taskCount": { - "pending": 60, - "completed": 15 - }, - "swimlanes": { - "enabled": false, - "showEmptySwimlanes": true, - "property": "tags", - "maxHeight": "500px", - "sortCriteria": "custom", - "customSortOrder": [ - { - "value": "first", - "index": 1 - }, - { - "value": "second", - "index": 2 - }, - { - "value": "third", - "index": 3 - } - ], - "groupAllRest": true, - "verticalHeaderUI": false - } + "property": "Status", + "key": "status", + "taskItemKey": "status" }, { - "name": "Only column filters", "index": 4, - "columns": [ - { - "id": 2050194674, - "index": 1, - "colType": "allPending", - "active": true, - "collapsed": false, - "name": "Backlogs", - "filters": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1768140055334-efs743ce1", - "groupCondition": "all", - "filters": [ - { - "id": "id-1768140055335-r98019pcb", - "property": "tags", - "condition": "isEmpty" - } - ] - } - ] - } - }, - { - "id": 850996788, - "index": 2, - "colType": "allPending", - "active": true, - "collapsed": false, - "name": "Important", - "filters": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1768140070366-ymlrpxfd5", - "groupCondition": "all", - "filters": [ - { - "id": "id-1768140070367-26fzfh3fi", - "property": "tags", - "condition": "contains", - "value": "#important" - } - ] - } - ] - } - }, - { - "id": 3370807172, - "index": 3, - "colType": "allPending", - "active": true, - "collapsed": false, - "name": "WIP", - "filters": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1768140095718-9u02uoxai", - "groupCondition": "all", - "filters": [ - { - "id": "id-1768140095719-sfcnddjpj", - "property": "tags", - "condition": "contains", - "value": "#wip" - } - ] - } - ] - } - }, - { - "id": 1957870296, - "index": 4, - "colType": "allPending", - "active": true, - "collapsed": false, - "name": "In Review", - "filters": { - "rootCondition": "any", - "filterGroups": [ - { - "id": "id-1768140118917-x50nyv0rm", - "groupCondition": "any", - "filters": [ - { - "id": "id-1768140118918-q6snn1uk7", - "property": "tags", - "condition": "contains", - "value": "#Test" - }, - { - "id": "id-1768140126093-wn5bwcpwz", - "property": "tags", - "condition": "contains", - "value": "#working" - } - ] - } - ] - }, - "workLimit": 3 - }, - { - "id": 4265650565, - "index": 5, - "colType": "completed", - "active": true, - "collapsed": false, - "name": "Completed", - "limit": 20 - } - ], - "hideEmptyColumns": false, - "showColumnTags": true, - "showFilteredTags": true, - "boardFilter": { - "rootCondition": "any", - "filterGroups": [] - }, - "swimlanes": { - "enabled": false, - "hideEmptySwimlanes": false, - "property": "tags", - "sortCriteria": "asc", - "minimized": [], - "maxHeight": "300px", - "verticalHeaderUI": false - }, - "taskCount": { - "pending": 63, - "completed": 14 - } + "property": "Priority", + "key": "priority", + "taskItemKey": "priority" + }, + { + "index": 5, + "property": "Tags", + "key": "tags", + "taskItemKey": "tags" + }, + { + "index": 6, + "property": "Time", + "key": "time", + "taskItemKey": "time" + }, + { + "index": 7, + "property": "Reminder", + "key": "reminder", + "taskItemKey": "reminder" + }, + { + "index": 8, + "property": "Created date", + "key": "created-date", + "taskItemKey": "createdDate" + }, + { + "index": 9, + "property": "Start date", + "key": "start-date", + "taskItemKey": "startDate" + }, + { + "index": 10, + "property": "Scheduled date", + "key": "scheduled-date", + "taskItemKey": "scheduledDate" + }, + { + "index": 11, + "property": "Due date", + "key": "due-date", + "taskItemKey": "due" + }, + { + "index": 12, + "property": "Depends on", + "key": "depends-on", + "taskItemKey": "dependsOn" + }, + { + "index": 13, + "property": "Completed date", + "key": "cancelled-date", + "taskItemKey": "cancelledDate" } ], - "globalSettings": { - "lang": "zh-TW", - "scanFilters": { - "files": { - "polarity": 3, - "values": [ - "Testing Indented Task Bug.md", - "/\\b2025-\\d{2}-\\d{2}\\b/" - ] - }, - "folders": { - "polarity": 3, - "values": [ - "Task Board/1.7.0", - "/Notes/", - "Bible" - ] - }, - "tags": { - "polarity": 3, - "values": [ - "#CS", - "#placeholder/author", - "*/seeding/*" - ] - }, - "frontMatter": { - "polarity": 3, - "values": [ - "[\"created\": 2025-02-27]", - "[\"tags\": #TEST]", - "[\"background\": yellow]" - ] - } + "showFrontmatterTagsOnCards": false, + "tasksCacheFilePath": "", + "notificationService": "none", + "actions": [ + { + "enabled": true, + "trigger": "Complete", + "type": "move", + "targetColumn": "Completed" + } + ], + "hiddenTaskProperties": [ + "createdDate", + "startDate", + "onCompletion", + "recurring" + ], + "autoAddUniqueID": true, + "uniqueIdCounter": 2258, + "experimentalFeatures": false, + "safeGuardFeature": true, + "lastViewHistory": { + "viewedType": "kanban", + "boardIndex": 1, + "settingTab": 5, + "taskId": "", + "boardFilePath": "Meta/Task_Board/Boards/Time Based Workflow.taskboard" + }, + "boundTaskCompletionToChildTasks": true, + "mapView": { + "background": "none", + "mapOrientation": "hor", + "optimizedRender": false, + "arrowDirection": "c2p", + "animatedEdges": false, + "scrollAction": "pan", + "showMinimap": true, + "renderVisibleNodes": false, + "edgeType": "smoothstep" + }, + "searchQuery": "", + "taskBoardFilesRegistry": { + "653162057": { + "boardId": "653162057", + "filePath": "My Project Board.taskboard", + "boardName": "My Project", + "boardDescription": "This is my personal project. This is a default board created by Task Board for you to kick start your journey with Task Board. Feel free to edit or create new boards." }, - "firstDayOfWeek": "2", - "ignoreFileNameDates": false, - "taskCompletionFormat": "2", - "taskCompletionDateTimePattern": "YYYY-MM-DD/HH:mm", - "dailyNotesPluginComp": true, - "universalDateFormat": "YYYY-MM-DD", - "taskCompletionInLocalTime": true, - "taskCompletionShowUtcOffset": false, - "autoAddDue": true, - "scanVaultAtStartup": false, - "dayPlannerPlugin": false, - "realTimeScanner": true, - "columnWidth": "300px", - "showHeader": true, - "showFooter": true, - "showVerticalScroll": false, - "tagColors": [ - { - "name": "Bug", - "color": "rgba(131, 10, 18, 0.84)", - "priority": 1 - }, - { - "name": "feat", - "color": "rgba(115, 15, 151, 0.9490196078431372)", - "priority": 3 - }, - { - "name": "Test", - "color": "rgba(142, 157, 24, 1)", - "priority": 4 - }, - { - "name": "working", - "color": "rgba(22, 85, 17, 1)", - "priority": 5 - }, - { - "name": "New", - "color": "rgba(16, 50, 117, 1)", - "priority": 6 - }, - { - "name": "pending", - "color": "rgba(64, 20, 144, 1)", - "priority": 7 - }, - { - "name": "done", - "color": "rgba(14, 117, 84, 1)", - "priority": 7 - }, - { - "name": "*/seeding/*", - "color": "rgba(15, 121, 110, 1)", - "priority": 8 - }, - { - "name": "first", - "color": "rgba(13, 103, 37, 1)", - "priority": 9 - } - ], - "editButtonAction": "popUp", - "openOnStartup": false, - "customStatuses": [ - { - "symbol": " ", - "name": "Incomplete", - "nextStatusSymbol": "x", - "availableAsCommand": false, - "type": "TODO" - }, - { - "symbol": "X", - "name": "Complete", - "nextStatusSymbol": " ", - "availableAsCommand": false, - "type": "DONE" - }, - { - "symbol": "/", - "name": "In Progress", - "nextStatusSymbol": "x", - "availableAsCommand": true, - "type": "IN_PROGRESS" - }, - { - "symbol": "-", - "name": "Cancelled", - "nextStatusSymbol": " ", - "availableAsCommand": true, - "type": "CANCELLED" - }, - { - "symbol": ">", - "name": "Deferred", - "nextStatusSymbol": "x", - "availableAsCommand": false, - "type": "TODO" - }, - { - "symbol": "!", - "name": "Important", - "nextStatusSymbol": "x", - "availableAsCommand": false, - "type": "TODO" - }, - { - "symbol": "x", - "name": "Finished", - "nextStatusSymbol": " ", - "availableAsCommand": false, - "type": "DONE" - } - ], - "showTaskWithoutMetadata": false, - "tagColorsType": "text", - "compatiblePlugins": { - "dailyNotesPlugin": false, - "tasksPlugin": false, - "reminderPlugin": false, - "dayPlannerPlugin": true, - "quickAddPlugin": false + "901052398": { + "boardId": "901052398", + "filePath": "TaskBoard-Template-1774097758616.taskboard", + "boardName": "My Project", + "boardDescription": "This is my personal project. This is a default board created by Task Board for you to kick start your journey with Task Board. Feel free to edit or create new boards." + }, + "1743944892": { + "boardId": "1743944892", + "filePath": "Meta/Task_Board/Boards/Time Based Workflow.taskboard", + "boardName": "Time Based Workflow", + "boardDescription": "" }, - "preDefinedNote": "Task_Board_Temp_Tasks.md", - "quickAddPluginDefaultChoice": "Task Board Temp Tasks", - "autoAddCreatedDate": true, - "autoAddUniversalDate": true, - "universalDate": "startDate", - "archivedTasksFilePath": "", - "showFileNameInCard": true, - "showFrontmatterTagsOnCards": true, - "tasksCacheFilePath": ".obsidian/plugins/task-board/tasks.json", - "notificationService": "obsiApp", - "frontmatterPropertyForReminder": "remind at", - "actions": [ - { - "enabled": true, - "trigger": "Complete", - "type": "move", - "targetColumn": "6" - } - ], - "cardSectionsVisibility": "hideBoth", - "hiddenTaskProperties": [], - "taskPropertyFormat": "3", - "taskNoteDefaultLocation": "", - "autoAddUniqueID": true, - "uniqueIdCounter": 586, - "experimentalFeatures": true, - "searchQuery": "", - "lastViewHistory": { - "viewedType": "kanban", - "boardIndex": 4, - "taskId": "", - "settingTab": 1 + "2248319344": { + "boardId": "2248319344", + "filePath": "Meta/Task_Board/Boards/Release 2.0.0.taskboard", + "boardName": "Release 2.0.0", + "boardDescription": "This board will be used to plan everything related to the next major version release of Task Board 2.0.0" }, - "taskNoteIdentifierTag": "task", - "doubleClickCardToEdit": "noteInTab", - "boundTaskCompletionToChildTasks": true, - "defaultStartTime": "23:59", - "archivedTBNotesFolderPath": "", - "frontmatterFormatting": [ - { - "index": 0, - "property": "ID", - "key": "id", - "taskItemKey": "id" - }, - { - "index": 1, - "property": "Title", - "key": "title", - "taskItemKey": "title" - }, - { - "index": 2, - "property": "Status", - "key": "status", - "taskItemKey": "status" - }, - { - "index": 3, - "property": "Priority", - "key": "priority", - "taskItemKey": "priority" - }, - { - "index": 4, - "property": "Tags", - "key": "tags", - "taskItemKey": "tags" - }, - { - "index": 5, - "property": "Time", - "key": "time", - "taskItemKey": "time" - }, - { - "index": 6, - "property": "Reminder", - "key": "reminder", - "taskItemKey": "reminder" - }, - { - "index": 7, - "property": "Created date", - "key": "created-date", - "taskItemKey": "createdDate" - }, - { - "index": 8, - "property": "Start date", - "key": "start-date", - "taskItemKey": "startDate" - }, - { - "index": 9, - "property": "Scheduled date", - "key": "scheduled", - "taskItemKey": "scheduledDate" - }, - { - "index": 10, - "property": "Due date", - "key": "due", - "taskItemKey": "due" - }, - { - "index": 11, - "property": "Depends on", - "key": "depends-on", - "taskItemKey": "dependsOn" - }, - { - "index": 12, - "property": "Cancelled date", - "key": "cancelled-date", - "taskItemKey": "cancelledDate" - }, - { - "index": 13, - "key": "completed-date", - "taskItemKey": "completionDate" - } - ], - "mapView": { - "background": "transparent", - "mapOrientation": "hor", - "optimizedRender": false, - "arrowDirection": "c2p", - "animatedEdges": false, - "scrollAction": "pan", - "showMinimap": true, - "renderVisibleNodes": true, - "edgeType": "default" + "2627237443": { + "boardId": "2627237443", + "filePath": "Meta/Task_Board/Boards/Status Based Workflow.taskboard", + "boardName": "Status Based Workflow", + "boardDescription": "" }, - "kanbanView": { - "lazyLoadingEnabled": true, - "initialTaskCount": 20, - "loadMoreCount": 10, - "scrollThresholdPercent": 80 + "3103563481": { + "boardId": "3103563481", + "filePath": "My Project Board.taskboard", + "boardName": "My Project", + "boardDescription": "This is my personal project. This is a default board created by Task Board for you to kick start your journey with Task Board. Feel free to edit or create new boards." }, - "safeGuardFeature": true, - "visiblePropertiesList": [ - "id", - "priority", - "tags", - "time", - "reminder", - "createdDate", - "startDate", - "scheduledDate", - "dueDate", - "completionDate", - "cancelledDate", - "dependsOn", - "filePath", - "status", - "checkbox" - ], - "taskCardStyle": "emoji", - "autoAddCompletedDate": true, - "autoAddCancelledDate": true, - "scanMode": "manual" + "3764827886": { + "boardId": "3764827886", + "filePath": "Meta/Task_Board/Boards/Tag Based Workflow.taskboard", + "boardName": "Tag Based Workflow", + "boardDescription": "" + } } } } \ No newline at end of file diff --git a/esbuild.config.mjs b/esbuild.config.mjs index cea70901..c6f57a88 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,6 @@ import esbuild from "esbuild"; import process from "process"; -import builtins from "builtin-modules"; +import { builtinModules } from "node:module"; const banner = `/* @@ -32,7 +32,7 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...builtins, + ...builtinModules, ], format: "cjs", loader: { diff --git a/main.ts b/main.ts index 458e923e..cde181e5 100644 --- a/main.ts +++ b/main.ts @@ -1,7 +1,14 @@ -// main.ts - +/** + * @name main.ts + * @path /main.ts + * The entry-point of this plugin. Initializes the plugin, initializes all the required + * internal managers and utils. + */ + +import { around } from "monkey-around"; import { App, + normalizePath, Notice, Plugin, PluginManifest, @@ -10,47 +17,65 @@ import { TFolder, WorkspaceLeaf, } from "obsidian"; +import { EmbedRegistry } from "obsidian-typings"; +import { parse } from "date-fns"; +import { t } from "i18next"; +import { + taskPropertyHidingExtension, + getTaskPropertyRegexPatterns, +} from "./src/editor-extensions/task-operations/property-hiding.js"; +import { + VIEW_TYPE_TASKBOARD, + TASKBOARD_FILE_EXTENSION, + OBSIDIAN_CLOSED_TIME_KEY, + DEFAULT_DATE_TIME_FORMAT, + CURRENT_PLUGIN_VERSION, + MANDATORY_SCAN_KEY, +} from "./src/interfaces/Constants.js"; +import { + taskPropertiesNames, + scanModeOptions, +} from "./src/interfaces/Enums.js"; import { - DEFAULT_SETTINGS, PluginDataJson, -} from "src/interfaces/GlobalSettings"; + DEFAULT_SETTINGS, +} from "./src/interfaces/GlobalSettings.js"; +import { TaskBoardIcon } from "./src/interfaces/Icons.js"; +import { bugReporterManagerInsatance } from "./src/managers/BugReporter.js"; +import { dragDropTasksManagerInsatance } from "./src/managers/DragDropTasksManager.js"; +import { RealTimeScanner } from "./src/managers/RealTimeScanner.js"; +import TaskBoardFileManager from "./src/managers/TaskBoardFileManager.js"; +import VaultScanner, { + fileTypeAllowedForScanning, +} from "./src/managers/VaultScanner.js"; +import { MergeBoardsModal } from "./src/modals/MergeBoardsModal.js"; +import { ModifiedFilesModal } from "./src/modals/ModifiedFilesModal.js"; +import { TaskBoardView } from "./src/obsidian_views/TaskBoardView.js"; +import { isReminderPluginInstalled } from "./src/services/CommunityPlugins.js"; +import { eventEmitter } from "./src/services/EventEmitter.js"; import { - openAddNewTaskInCurrentFileModal, openAddNewTaskModal, openAddNewTaskNoteModal, + openAddNewTaskInCurrentFileModal, + openBoardsExplorerModal, openScanVaultModal, -} from "src/services/OpenModals"; - -import { TaskBoardView } from "./src/views/TaskBoardView"; -import { RealTimeScanner } from "src/managers/RealTimeScanner"; -import VaultScanner, { - fileTypeAllowedForScanning, -} from "src/managers/VaultScanner"; -import { TaskBoardIcon } from "src/interfaces/Icons"; -import { TaskBoardSettingTab } from "./src/settings/TaskBoardSettingTab"; -import { ModifiedFilesModal } from "src/modals/ModifiedFilesModal"; -import { - newReleaseVersion, - VIEW_TYPE_TASKBOARD, -} from "src/interfaces/Constants"; -import { isReminderPluginInstalled } from "src/services/CommunityPlugins"; -import { loadTranslationsOnStartup, t } from "src/utils/lang/helper"; -import { TaskBoardApi } from "src/taskboardAPIs"; -import { TasksPluginApi } from "src/services/tasks-plugin/api"; -import { - getTaskPropertyRegexPatterns, - taskPropertyHidingExtension, -} from "src/editor-extensions/task-operations/property-hiding"; +} from "./src/services/OpenModals.js"; +import { TasksPluginApi } from "./src/services/tasks-plugin/api.js"; +import { isTasksPluginEnabled } from "./src/services/tasks-plugin/helpers.js"; import { - fetchTasksPluginCustomStatuses, - isTasksPluginEnabled, -} from "src/services/tasks-plugin/helpers"; -import { scanModeOptions, taskPropertiesNames } from "src/interfaces/Enums"; -import { migrateSettings } from "src/settings/SettingSynchronizer"; -import { dragDropTasksManagerInsatance } from "src/managers/DragDropTasksManager"; -import { eventEmitter } from "src/services/EventEmitter"; -import { bugReporterManagerInsatance } from "src/managers/BugReporter"; - + checkAndNotifyV2MigrationsRequired, + openMigrationModal, +} from "./src/settings/2_x_x_Migrations/MigrationUtils.js"; +import { migrateSettings } from "./src/settings/SettingSynchronizer.js"; +import { TaskBoardSettingTab } from "./src/settings/TaskBoardSettingTab.js"; +import { TaskBoardApi } from "./src/taskboardAPIs.js"; +import { getCurrentLocalDateTimeString } from "./src/utils/DateTimeCalculations.js"; +import { loadTranslationsOnStartup } from "./src/utils/lang/helper.js"; +import { DEFAULT_BOARD } from "./src/interfaces/BoardConfigs.js"; + +/** + * The entry-point of this project. + */ export default class TaskBoard extends Plugin { app: App; plugin: TaskBoard; @@ -58,14 +83,16 @@ export default class TaskBoard extends Plugin { settings: PluginDataJson = DEFAULT_SETTINGS; vaultScanner: VaultScanner; realTimeScanner: RealTimeScanner; + taskBoardFileManager: TaskBoardFileManager; // taskBoardFileStack: string[] = []; - private _editorModified: boolean = false; // Private backing field // currentModifiedFile: TFile | null; // fileUpdatedUsingModal: string; IstasksJsonDataChanged: boolean; isI18nInitialized: boolean; - private _leafIsActive: boolean; // Private property to track leaf state + private ribbonIconEl: HTMLElement | null; // Store ribbonIconEl globally for reference + private _editorModified: boolean = false; // Private backing field + private _leafIsActive: boolean; // Private property to track leaf state // Public getter/setter for editorModified that emits events get editorModified(): boolean { @@ -88,13 +115,16 @@ export default class TaskBoard extends Plugin { private deleteProcessingTimer: NodeJS.Timeout | null = null; private createProcessingTimer: NodeJS.Timeout | null = null; private currentProgressNotice: Notice | null = null; - private readonly QUEUE_DELAY = 1000; // Delay in ms before starting to process queue + private readonly QUEUE_DELAY = 2000; // Delay in ms before starting to process queue private readonly PROCESSING_INTERVAL = 100; // Delay between processing each file + v2MigrationsRequired = false; + constructor(app: App, menifest: PluginManifest) { super(app, menifest); this.plugin = this; - this.app = this.plugin.app; + this.app = app; + this.plugin.app = app; this.view = null; this.settings = DEFAULT_SETTINGS; this.vaultScanner = new VaultScanner(this.app, this.plugin); @@ -103,6 +133,7 @@ export default class TaskBoard extends Plugin { this.plugin, this.vaultScanner, ); + this.taskBoardFileManager = new TaskBoardFileManager(this.plugin); this.editorModified = false; // this.currentModifiedFile = null; // this.fileUpdatedUsingModal = ""; @@ -119,26 +150,32 @@ export default class TaskBoard extends Plugin { async onload() { console.log("Task Board : Loading..."); + // this.getLanguage(); + await loadTranslationsOnStartup(this); + // NOTE : I feel, if these singleton instances needs the latest version of 'this', then they might show some unexpected behavior as I am not updating the 'this' inside those singleton instances latest during the plugin life-cycle. - dragDropTasksManagerInsatance.setPlugin(this); bugReporterManagerInsatance.setPlugin(this); + // Migrations for updating from v1.x.x version series to v2.x.x series version + this.v2MigrationsRequired = + await checkAndNotifyV2MigrationsRequired(this); + // Loads settings data and creating the Settings Tab in main Setting await this.loadSettings(); - this.runOnPluginUpdate(); + if (!this.v2MigrationsRequired) await this.runOnPluginUpdate(); this.addSettingTab(new TaskBoardSettingTab(this.app, this)); - // this.getLanguage(); - - await loadTranslationsOnStartup(this); - await this.vaultScanner.initializeTasksCache(); + // Register the Kanban view + this.registerTaskBoardView(); + // Register events and commands only on Layout is ready this.app.workspace.onLayoutReady(() => { - console.log("Task Board : Running onLayoutReady..."); this.compatiblePluginsAvailabilityCheck(); + dragDropTasksManagerInsatance.setPlugin(this); + //Creates a Icon on Ribbon Bar (after i18n is initialized) this.getRibbonIcon(); @@ -151,9 +188,6 @@ export default class TaskBoard extends Plugin { // For non-realtime scanning and scanning last modified files this.createLocalStorageAndScanModifiedFiles(); - // Register the Kanban view - this.registerTaskBoardView(); - // Run openAtStartup if openOnStartup is true this.openAtStartup(); @@ -166,11 +200,10 @@ export default class TaskBoard extends Plugin { // Register markdown post processor for hiding task properties this.registerReadingModePostProcessor(); - setTimeout(() => this.findModifiedFilesOnAppAbsense(), 10000); + this.taskBoardFileManager.validateBoardFiles(); - console.log("Task Board : onLayoutReady FINISHED."); + setTimeout(() => this.findModifiedFilesOnAppAbsense(), 10000); }); - console.log("Task Board : onload funcion FINISHED."); } onunload() { @@ -178,61 +211,110 @@ export default class TaskBoard extends Plugin { // deleteAllLocalStorageKeys(); // TODO : Enable this while production build. This is disabled for testing purpose because the data from localStorage is required for testing. // onUnloadSave(this.plugin); + + // Obsidian already does this, no need to manually detach. // this.app.workspace.detachLeavesOfType(VIEW_TYPE_TASKBOARD); } - async activateView(leafLayout: string) { - let leaf: WorkspaceLeaf | null = null; - const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_TASKBOARD); + /** + * Opens the Task Board view using either the last viewed board file or opens the board file + * whose filePath has been passed. Most of the time, this function will try to find an existing + * leaf for the specific board file. If user specifically wants to have a duplicate leaf, pass + * the {@link duplicate} as true. + * + * @param leafLayout - Where to open the board leaf/tab. New tab or new window. + * @param duplicate - Whether to re-use already opened leaf or create a new one. + * This will be true in only special cases, when user wants to specifical open a duplicate. + * @param filePath (OPTIONAL) - The file path of the board to open. If no filePath has been + * provided then will open the last viewed board. + */ + async activateView( + leafLayout: string, + duplicate: boolean, + filePath?: string, + ) { + try { + let leaf: WorkspaceLeaf | null = null; + const leaves = + this.app.workspace.getLeavesOfType(VIEW_TYPE_TASKBOARD); + + function isFromMainWindow( + leaf: WorkspaceLeaf, + ): boolean | undefined { + if (filePath) { + const state = leaf.getViewState(); + if ( + state?.state?.filePath && + state?.state?.filePath !== filePath + ) { + return false; + } + } - function isFromMainWindow(leaf: WorkspaceLeaf): boolean | undefined { - if (!leaf.view.containerEl.ownerDocument.defaultView) return; - return "Notice" in leaf.view.containerEl.ownerDocument.defaultView; - } + if (!leaf.view.containerEl.ownerDocument.defaultView) return; + return ( + "Notice" in leaf.view.containerEl.ownerDocument.defaultView + ); + } - // Separate leaves into MainWindow and SeparateWindow categories - const mainWindowLeaf = leaves.find((leaf) => isFromMainWindow(leaf)); - const separateWindowLeaf = leaves.find( - (leaf) => !isFromMainWindow(leaf), - ); + // Separate leaves into MainWindow and SeparateWindow categories + const mainWindowLeaf = leaves.find((leaf) => + isFromMainWindow(leaf), + ); + const separateWindowLeaf = leaves.find( + (leaf) => !isFromMainWindow(leaf), + ); - if (leafLayout === "icon") { - // Focus on any existing leaf, prioritizing MainWindow - leaf = - mainWindowLeaf || - separateWindowLeaf || - this.app.workspace.getLeaf("tab"); - } else if (leafLayout === "tab") { - // Check if a leaf exists in MainWindow - if (mainWindowLeaf) { - // Prevent duplicate in MainWindow - leaf = mainWindowLeaf; + if (leafLayout === "icon") { + // Focus on any existing leaf, prioritizing MainWindow + leaf = + mainWindowLeaf || + separateWindowLeaf || + this.app.workspace.getLeaf("tab"); + } else if (leafLayout === "tab") { + // Check if a leaf exists in MainWindow + if (mainWindowLeaf && !duplicate) { + // Prevent duplicate in MainWindow + leaf = mainWindowLeaf; + } else { + // Allow opening a new leaf in MainWindow + leaf = this.app.workspace.getLeaf("tab"); + } + } else if (leafLayout === "window") { + // Check if a leaf exists in SeparateWindow + if (separateWindowLeaf) { + // Prevent duplicate in SeparateWindow + leaf = separateWindowLeaf; + } else { + // Allow opening a new leaf in SeparateWindow + leaf = this.app.workspace.getLeaf("window"); + } } else { - // Allow opening a new leaf in MainWindow + // Default behavior: open in MainWindow leaf = this.app.workspace.getLeaf("tab"); } - } else if (leafLayout === "window") { - // Check if a leaf exists in SeparateWindow - if (separateWindowLeaf) { - // Prevent duplicate in SeparateWindow - leaf = separateWindowLeaf; - } else { - // Allow opening a new leaf in SeparateWindow - leaf = this.app.workspace.getLeaf("window"); - } - } else { - // Default behavior: open in MainWindow - leaf = this.app.workspace.getLeaf("tab"); - } - // Open or focus the leaf - if (leaf) { - this.leafIsActive = true; - await leaf.setViewState({ - type: VIEW_TYPE_TASKBOARD, - active: true, - }); - this.app.workspace.revealLeaf(leaf); + // Open or focus the leaf + if (leaf) { + this.leafIsActive = true; + leaf.setEphemeralState({ taskboardFilePath: filePath ?? "" }); + + await leaf.setViewState({ + type: VIEW_TYPE_TASKBOARD, + active: true, + state: { + filePath: filePath ?? "", + }, + }); + + this.app.workspace.revealLeaf(leaf); + } + } catch (error) { + bugReporterManagerInsatance.addToLogs( + 202, + `Error opening the board: ${error}`, + "main.ts/activateView", + ); } } @@ -242,7 +324,7 @@ export default class TaskBoard extends Plugin { TaskBoardIcon, t("open-task-board") ?? "Open task board", () => { - this.activateView("icon"); + this.activateView("icon", false); // this.app.workspace.ensureSideLeaf(VIEW_TYPE_TASKBOARD, "right", { // active: true, @@ -274,11 +356,19 @@ export default class TaskBoard extends Plugin { } async saveSettings(newSetting?: PluginDataJson) { - if (newSetting) { - this.settings = newSetting; - await this.saveData(newSetting); - } else { - await this.saveData(this.settings); + try { + if (newSetting) { + this.settings = newSetting; + await this.saveData(newSetting); + } else { + await this.saveData(this.settings); + } + } catch (err) { + bugReporterManagerInsatance.addToLogs( + 140, + String(err), + "main.ts/saveSettings", + ); } } @@ -287,12 +377,12 @@ export default class TaskBoard extends Plugin { // if (obsidianLang && obsidianLang in langCodes) { // localStorage.setItem("taskBoardLang", obsidianLang); - // this.settings.data.globalSettings.lang = obsidianLang; + // this.settings.data.lang = obsidianLang; // this.saveSettings(); // } else { // localStorage.setItem( // "taskBoardLang", - // // this.settings.data.globalSettings.lang + // // this.settings.data.lang // "en" // ); // } @@ -310,6 +400,36 @@ export default class TaskBoard extends Plugin { return this.view; }); + this.registerExtensions( + [TASKBOARD_FILE_EXTENSION], + VIEW_TYPE_TASKBOARD, + ); + + // Monkey-patch WorkspaceLeaf.setViewState to intercept .taskboard file clicks + this.registerMonkeyPatchForTaskboardFiles(); + + if (this.settings.data.experimentalFeatures) { + // @ts-ignore + const embedRegistry = this.app.embedRegistry as EmbedRegistry; + if ( + !embedRegistry?.isExtensionRegistered(TASKBOARD_FILE_EXTENSION) + ) { + embedRegistry?.registerExtension( + TASKBOARD_FILE_EXTENSION, + (context, file, _) => { + // @ts-ignore + return new TaskBoardEmbedComponent( + context.containerEl, + this, + // @ts-ignore + file, + context.containerEl.getAttr("alt") || undefined, + ) as any; + }, + ); + } + } + // Register AddOrEditTask view (can be opened in tabs or popout windows) // this.registerView(VIEW_TYPE_ADD_OR_EDIT_TASK, (leaf) => { // console.log("Leaf returned by registerView :", leaf); @@ -328,37 +448,67 @@ export default class TaskBoard extends Plugin { // }); } + /** + * Monkey-patch WorkspaceLeaf.setViewState to intercept .taskboard file clicks + * When a user clicks on a .taskboard file in the File Navigator, this intercepts + * the default markdown view and opens it in the TaskBoard custom view instead, + * while preserving the file path in the view state + */ + private registerMonkeyPatchForTaskboardFiles() { + // Use monkey-around to safely patch WorkspaceLeaf.prototype.setViewState + // This allows multiple plugins to patch the same method without conflicts + const unregisterPatch = around(WorkspaceLeaf.prototype, { + setViewState: (next) => + function (this: WorkspaceLeaf, state: any, eState?: any) { + const isTaskBoardView = state.type === VIEW_TYPE_TASKBOARD; + const filePath = state.state?.file as string | undefined; + const isTaskboardFile = + filePath && filePath.endsWith(".taskboard"); + + if (isTaskBoardView && isTaskboardFile) { + // Store the file path directly on the leaf instance for immediate access + (this as any).taskboardFilePath = filePath; + + // Also set ephemeral state for safety + this.setEphemeralState({ taskboardFilePath: filePath }); + } + + // Call the next method in the chain (original or other patches) + return next.call(this, state, eState); + }, + }); + + // Register cleanup handler to unregister the patch when plugin unloads + // This prevents memory leaks and ensures the patch is properly removed + this.register(unregisterPatch); + } + registerEditorExtensions() { // TODO : The below editor extension will not going to be released in the upcoming version, will plan it for the next version. // Register task gutter extension // this.registerEditorExtension(taskGutterExtension(this.app, this)); // Register task property hiding extension - const hiddenProperties = - this.settings.data.globalSettings?.hiddenTaskProperties || []; + const hiddenProperties = this.settings.data?.hiddenTaskProperties || []; if (hiddenProperties.length > 0) { this.registerEditorExtension(taskPropertyHidingExtension(this)); } } registerReadingModePostProcessor() { - const hiddenProperties = - this.settings.data.globalSettings?.hiddenTaskProperties || []; + const hiddenProperties = this.settings.data?.hiddenTaskProperties || []; if (hiddenProperties.length === 0) { return; } const tasksPlugin = new TasksPluginApi(this); if (!tasksPlugin.isTasksPluginEnabled()) { this.registerMarkdownPostProcessor((element, context) => { - // console.log("Element : ", element, "\nContent :", context); // Only process if we have properties to hide - // Find all list items that could be tasks const listItems = element.querySelectorAll("li"); listItems.forEach((listItem) => { // const textContent = listItem.textContent || ""; - // console.log("Text Content :", textContent); // Check if this is a task (starts with checkbox syntax) if (listItem.querySelector(".contains-task-list")) { this.hidePropertiesInElement( @@ -538,7 +688,7 @@ export default class TaskBoard extends Plugin { hiddenProperties.forEach((property) => { const pattern = getTaskPropertyRegexPatterns( property, - this.settings.data.globalSettings?.taskPropertyFormat, + this.settings.data?.taskPropertyFormat, ); if (pattern.test(content)) { content = content.replace(pattern, (match) => { @@ -566,9 +716,9 @@ export default class TaskBoard extends Plugin { } openAtStartup() { - if (!this.settings.data.globalSettings.openOnStartup) return; + if (!this.settings.data.openOnStartup) return; - this.activateView("icon"); + this.activateView("icon", false); } registerTaskBoardStatusBar() { @@ -578,12 +728,12 @@ export default class TaskBoard extends Plugin { // statusBarItemEl.setText("Next task in # min"); } - registerCommands() { + async registerCommands() { this.addCommand({ id: "add-new-task", name: t("add-new-task"), callback: () => { - openAddNewTaskModal(this.app, this.plugin); + openAddNewTaskModal(this.plugin); }, }); this.addCommand({ @@ -619,25 +769,50 @@ export default class TaskBoard extends Plugin { id: "open-task-board", name: t("open-task-board"), callback: () => { - this.activateView("tab"); + this.activateView("tab", false); }, }); this.addCommand({ id: "open-task-board-new-window", name: t("open-task-board-in-new-window"), callback: () => { - this.activateView("window"); + this.activateView("window", false); + }, + }); + this.addCommand({ + id: "open-task-boards-explorer", + name: t("open-task-boards-explorer"), + callback: () => { + openBoardsExplorerModal(this); }, }); this.addCommand({ id: "open-scan-vault-modal", name: t("open-scan-vault-modal"), callback: () => { - openScanVaultModal(this.app, this.plugin); + openScanVaultModal(this.plugin); + }, + }); + this.addCommand({ + id: "merge-boards", + name: "Merge Boards", + callback: () => { + new MergeBoardsModal(this.app, { + plugin: this, + taskBoardFileManager: this.taskBoardFileManager, + }).open(); }, }); - // // TODO : Remove this command before publishing, DEV commands + if (this.v2MigrationsRequired) { + this.addCommand({ + id: "open-migration-modal", + name: "Open migration modal", + callback: () => { + openMigrationModal(this.plugin); + }, + }); + } // this.addCommand({ // id: "4", // name: "DEV : Save Data from sessionStorage to Disk", @@ -661,8 +836,8 @@ export default class TaskBoard extends Plugin { /** * Add a file to the rename queue and schedule processing * @private - * @param {TAbstractFile} file - The file to add to the queue - * @param {string} oldPath - The old path of the file + * @param file - The file to add to the queue + * @param oldPath - The old path of the file */ private queueFileForRename(file: TAbstractFile, oldPath: string) { // Only queue TFile objects (not folders) that are allowed for scanning @@ -690,61 +865,72 @@ export default class TaskBoard extends Plugin { return; } - const archivedPath = - this.settings.data.globalSettings.archivedTBNotesFolderPath; - const totalFiles = this.renameQueue.length; - - // Show progress notice - this.currentProgressNotice = new Notice( - `Processing renamed files: 0/${totalFiles}`, - 0, + const archivedPath = normalizePath( + this.settings.data.archivedTBNotesFolderPath, ); + let allowedFiles = this.renameQueue.filter((fileData) => + fileTypeAllowedForScanning(this.settings.data, fileData.file), + ); + const totalFilesLength = allowedFiles.length; - let processed = 0; - while (this.renameQueue.length > 0) { - const { file, oldPath } = this.renameQueue.shift()!; + // Empty the global queue + this.renameQueue = []; - try { - if ( - fileTypeAllowedForScanning( - this.plugin.settings.data.globalSettings, - file, - ) - ) { + if (totalFilesLength > 0) { + // Show progress notice + this.currentProgressNotice = new Notice( + `Processing renamed files: 0/${totalFilesLength}`, + 0, + ); + + let processed = 0; + while (allowedFiles.length > 0) { + const { file, oldPath } = allowedFiles.shift()!; + + try { this.realTimeScanner.onFileRenamed( file, oldPath, archivedPath, ); + processed++; + + // Update progress notice + this.currentProgressNotice.messageEl.textContent = `Task Board : Processing renamed files: ${processed}/${totalFilesLength}`; + } catch (error) { + this.currentProgressNotice?.hide(); + // this.currentProgressNotice = null; + bugReporterManagerInsatance.addToLogs( + 162, + String(error), + "main.ts/processRenameQueue", + ); + } + + // Add delay between processing each file to prevent blocking UI + if (allowedFiles.length > 0) { + await new Promise((resolve) => + setTimeout(resolve, this.PROCESSING_INTERVAL), + ); } - processed++; - - // Update progress notice - this.currentProgressNotice.messageEl.textContent = `Task Board : Processing renamed files: ${processed}/${totalFiles}`; - } catch (error) { - console.error( - `Error processing renamed file ${file.path}:`, - error, - ); } - // Add delay between processing each file to prevent blocking UI - if (this.renameQueue.length > 0) { - await new Promise((resolve) => - setTimeout(resolve, this.PROCESSING_INTERVAL), + // Hide progress notice after completion + this.currentProgressNotice?.hide(); + this.currentProgressNotice = null; + + this.plugin.vaultScanner.saveTasksToJsonCache(); + eventEmitter.emit("REFRESH_BOARD"); + + if (processed > 0) { + new Notice( + `✓ Task Board : Finished processing ${totalFilesLength} renamed file(s)`, ); } } - this.plugin.vaultScanner.saveTasksToJsonCache(); - eventEmitter.emit("REFRESH_BOARD"); - - // Hide progress notice after completion - this.currentProgressNotice?.hide(); - this.currentProgressNotice = null; - new Notice( - `✓ Task Board : Finished processing ${totalFiles} renamed file(s)`, - ); + if (this.renameProcessingTimer) + clearTimeout(this.renameProcessingTimer); } /** @@ -756,13 +942,14 @@ export default class TaskBoard extends Plugin { this.deleteQueue.push(file); // Clear existing timer and set a new one - if (this.deleteProcessingTimer) { - clearTimeout(this.deleteProcessingTimer); + if (!this.deleteProcessingTimer) { + this.deleteProcessingTimer = setTimeout(() => { + this.processDeleteQueue(); + }, this.QUEUE_DELAY); + } else { + // NOTE : I think there is no need to remove the Timout created, in 2 seconds, all the Obsidians triggers should finish, for the Task Board's processing to start. + // clearTimeout(this.deleteProcessingTimer); } - - this.deleteProcessingTimer = setTimeout(() => { - this.processDeleteQueue(); - }, this.QUEUE_DELAY); } } @@ -776,55 +963,58 @@ export default class TaskBoard extends Plugin { return; } - const totalFiles = this.deleteQueue.length; - - // Show progress notice - this.currentProgressNotice = new Notice( - `Processing deleted files: 0/${totalFiles}`, - 0, + let allowedFiles = this.deleteQueue.filter((file: TAbstractFile) => + fileTypeAllowedForScanning(this.settings.data, file), ); + const totalFilesLength = allowedFiles.length; - let processed = 0; - while (this.deleteQueue.length > 0) { - const file = this.deleteQueue.shift()!; + if (allowedFiles.length > 0) { + // Show progress notice + this.currentProgressNotice = new Notice( + `Processing deleted files: 0/${totalFilesLength}`, + 0, + ); - try { - if ( - fileTypeAllowedForScanning( - this.plugin.settings.data.globalSettings, - file, - ) - ) { + let processed = 0; + while (allowedFiles.length > 0) { + const file = allowedFiles.shift()!; + + try { this.realTimeScanner.onFileDeleted(file); + processed++; + + // Update progress notice + this.currentProgressNotice.messageEl.textContent = `Task Board : Processing deleted files: ${processed}/${totalFilesLength}`; + } catch (error) { + this.currentProgressNotice?.hide(); + // this.currentProgressNotice = null; + bugReporterManagerInsatance.addToLogs( + 163, + String(error), + "main.ts/processDeleteQueue", + ); + } + + // Add delay between processing each file to prevent blocking UI + if (allowedFiles.length > 0) { + await new Promise((resolve) => + setTimeout(resolve, this.PROCESSING_INTERVAL), + ); } - processed++; - - // Update progress notice - this.currentProgressNotice.messageEl.textContent = `Task Board : Processing deleted files: ${processed}/${totalFiles}`; - } catch (error) { - console.error( - `Error processing deleted file ${file.path}:`, - error, - ); } + // Hide progress notice after completion + this.currentProgressNotice?.hide(); + this.currentProgressNotice = null; + + this.plugin.vaultScanner.saveTasksToJsonCache(); + eventEmitter.emit("REFRESH_COLUMN"); - // Add delay between processing each file to prevent blocking UI - if (this.deleteQueue.length > 0) { - await new Promise((resolve) => - setTimeout(resolve, this.PROCESSING_INTERVAL), + if (processed > 0) { + new Notice( + `✓ Task Board : Finished processing ${totalFilesLength} deleted file(s)`, ); } } - - this.plugin.vaultScanner.saveTasksToJsonCache(); - eventEmitter.emit("REFRESH_COLUMN"); - - // Hide progress notice after completion - this.currentProgressNotice?.hide(); - this.currentProgressNotice = null; - new Notice( - `✓ Task Board : Finished processing ${totalFiles} deleted file(s)`, - ); } /** @@ -836,13 +1026,14 @@ export default class TaskBoard extends Plugin { this.createQueue.push(file); // Clear existing timer and set a new one - if (this.createProcessingTimer) { - clearTimeout(this.createProcessingTimer); + if (!this.createProcessingTimer) { + this.createProcessingTimer = setTimeout(() => { + this.processCreateQueue(); + }, this.QUEUE_DELAY); + } else { + // NOTE : I think there is no need to remove the Timout created, in 2 seconds, all the Obsidians triggers should finish, for the Task Board's processing to start. + // clearTimeout(this.createProcessingTimer); } - - this.createProcessingTimer = setTimeout(() => { - this.processCreateQueue(); - }, this.QUEUE_DELAY); } /** @@ -855,54 +1046,238 @@ export default class TaskBoard extends Plugin { return; } - const totalFiles = this.createQueue.length; - - // Show progress notice - this.currentProgressNotice = new Notice( - `Task Board : Processing created files: 0/${totalFiles}`, - 0, + let allowedFiles = this.createQueue.filter((file: TFile) => + fileTypeAllowedForScanning(this.settings.data, file), ); + const totalFilesLength = allowedFiles.length; - this.plugin.vaultScanner.refreshTasksFromFiles(this.createQueue, false); + this.plugin.vaultScanner.refreshTasksFromFiles(allowedFiles, false); - let processed = 0; - while (this.createQueue.length > 0) { - const file = this.createQueue.shift()!; + // Show progress notice only if the files are more than 10 + if (totalFilesLength > 10) { + this.currentProgressNotice = new Notice( + `Task Board : Processing created files: 0/${totalFilesLength}`, + 0, + ); + let processed = 0; + while (allowedFiles.length > 0) { + const file = allowedFiles.shift()!; + + try { + // if ( + // fileTypeAllowedForScanning( + // this.plugin.settings.data.globalSettings, + // file + // ) + // ) { + // await this.realTimeScanner.processAllUpdatedFiles(file); + // } + processed++; + + // Update progress notice + this.currentProgressNotice.messageEl.textContent = `Task Board : Processing created files: ${processed}/${totalFilesLength}`; + } catch (error) { + this.currentProgressNotice?.hide(); + // this.currentProgressNotice = null; + bugReporterManagerInsatance.addToLogs( + 164, + String(error), + "main.ts/processCreateQueue", + ); + } - try { - // if ( - // fileTypeAllowedForScanning( - // this.plugin.settings.data.globalSettings, - // file - // ) - // ) { - // await this.realTimeScanner.processAllUpdatedFiles(file); - // } - processed++; - - // Update progress notice - this.currentProgressNotice.messageEl.textContent = `Task Board : Processing created files: ${processed}/${totalFiles}`; - } catch (error) { - console.error( - `Error processing created file ${file.path}:`, - error, - ); + // Add delay between processing each file to prevent blocking UI + if (allowedFiles.length > 0) { + await new Promise((resolve) => + setTimeout(resolve, this.PROCESSING_INTERVAL), + ); + } } - // Add delay between processing each file to prevent blocking UI - if (this.createQueue.length > 0) { - await new Promise((resolve) => - setTimeout(resolve, this.PROCESSING_INTERVAL), + // Hide progress notice after completion + this.currentProgressNotice?.hide(); + this.currentProgressNotice = null; + if (processed > 0) { + new Notice( + `✓ Task Board : Finished processing ${totalFilesLength} created file(s)`, ); } } + } - // Hide progress notice after completion - this.currentProgressNotice?.hide(); - this.currentProgressNotice = null; - new Notice( - `✓ Task Board : Finished processing ${totalFiles} created file(s)`, - ); + /** + * Runs on plugin load/Obsidian startup time and find all the files which where + * modified (edited/renamed/deleted) between the time when Obsidian was last closed + * till now. + */ + async findModifiedFilesOnAppAbsense() { + const storedTime = this.app.loadLocalStorage( + OBSIDIAN_CLOSED_TIME_KEY, + ) as string | undefined; + + let OBSIDIAN_CLOSED_TIME: Date | undefined; + + if (storedTime) { + OBSIDIAN_CLOSED_TIME = parse( + storedTime, + DEFAULT_DATE_TIME_FORMAT, + new Date(), + ); + } else { + OBSIDIAN_CLOSED_TIME = parse( + this.vaultScanner.tasksCache.Modified_at, + DEFAULT_DATE_TIME_FORMAT, + new Date(), + ); + } + + if (OBSIDIAN_CLOSED_TIME) { + let filesScannedCount = 0; + const modifiedCreatedRenamedFiles = this.app.vault + .getFiles() + .filter((file) => { + filesScannedCount++; + return ( + file.stat.mtime > OBSIDIAN_CLOSED_TIME!.getTime() || + file.stat.ctime > OBSIDIAN_CLOSED_TIME!.getTime() + ); + }); + + // Find deleted files by comparing cache with current vault files + const currentFilesPaths = new Set( + this.app.vault.getFiles().map((file) => file.path), + ); + const cachedFilesPaths = Object.keys( + this.vaultScanner.tasksCache.Pending || {}, + ).concat(Object.keys(this.vaultScanner.tasksCache.Completed || {})); + const deletedFiles = new Set( + cachedFilesPaths.filter( + (filePath) => !currentFilesPaths.has(filePath), + ), + ); + const deletedFilesList = [...deletedFiles]; + + const changed_files = modifiedCreatedRenamedFiles.filter((file) => + fileTypeAllowedForScanning(this.plugin.settings.data, file), + ); + const totalFilesLength = + changed_files.length + deletedFilesList.length; + + if (totalFilesLength > 0) { + const scanAllModifiedFiles = () => { + this.plugin.vaultScanner + .refreshTasksFromFiles(changed_files, false) + .then(async () => { + if (deletedFilesList.length > 0) { + await this.plugin.vaultScanner.deleteCacheForFiles( + deletedFilesList, + ); + } + }); + }; + + if (this.settings.data.showModifiedFilesNotice) { + const modifiedFilesNotice = new Notice( + createFragment((f) => { + f.createDiv("bugReportNotice", (el) => { + el.createEl("p", { + text: `Task Board : ${totalFilesLength} files has been modified when Obsidian was inactive.`, + }); + el.createEl("button", { + text: t("show-me"), + cls: "reportBugButton", + onclick: () => { + // el.hide(); + + // Open a modal and show all these file names with their modified date-time in a nice UI. + const modifiedFilesModal = + new ModifiedFilesModal(this.app, { + modifiedFiles: changed_files, + deletedFiles: deletedFilesList, + }); + modifiedFilesModal.open(); + }, + }); + el.createEl("button", { + text: t("scan-them"), + cls: "ignoreBugButton", + onclick: async () => { + try { + modifiedFilesNotice.hide(); + + // Show progress notice + this.currentProgressNotice = + new Notice( + `Task Board : Processing modified files: 0/${totalFilesLength}`, + 0, + ); + + scanAllModifiedFiles(); + + let modifiedFilesQueueLength = + changed_files?.length ?? 0; + + let processed = 0; + while ( + modifiedFilesQueueLength > 0 + ) { + modifiedFilesQueueLength = + modifiedFilesQueueLength - + 1; + + processed++; + + // Update progress notice + this.currentProgressNotice.messageEl.textContent = `Task Board : Processing created files: ${processed}/${totalFilesLength}`; + + // Add delay between processing each file to prevent blocking UI + if ( + modifiedFilesQueueLength > 0 + ) { + await new Promise( + (resolve) => + setTimeout( + resolve, + this + .PROCESSING_INTERVAL, + ), + ); + } + } + + // Hide progress notice after completion + this.currentProgressNotice?.hide(); + this.currentProgressNotice = null; + new Notice( + `✓ Task Board : Finished processing ${totalFilesLength} created file(s)`, + ); + } catch (error) { + this.currentProgressNotice?.hide(); + bugReporterManagerInsatance.addToLogs( + 165, + String(error), + "main.ts/findModifiedFilesOnAppAbsense", + ); + } + }, + }); + }); + }), + 0, + ); + + modifiedFilesNotice.messageEl.onClickEvent((e) => { + if (e.target instanceof HTMLButtonElement) { + e.stopPropagation(); + e.preventDefault(); + e.stopImmediatePropagation(); + } + }); + } else { + scanAllModifiedFiles(); + } + } + } } /** @@ -911,17 +1286,13 @@ export default class TaskBoard extends Plugin { registerEvents() { this.registerEvent( this.app.vault.on("modify", async (file: TAbstractFile) => { - console.log("Modify event is fired..."); if ( - fileTypeAllowedForScanning( - this.plugin.settings.data.globalSettings, - file, - ) + fileTypeAllowedForScanning(this.plugin.settings.data, file) ) { if (file instanceof TFile) { if ( - this.plugin.settings.data.globalSettings - .scanMode === scanModeOptions.REAL_TIME + this.plugin.settings.data.scanMode === + scanModeOptions.REAL_TIME ) { this.vaultScanner.refreshTasksFromFiles( [file], @@ -938,21 +1309,18 @@ export default class TaskBoard extends Plugin { ); this.registerEvent( this.app.vault.on("rename", (file, oldPath) => { - console.log("Rename event is fired..."); // Queue the file for processing instead of processing immediately this.queueFileForRename(file, oldPath); }), ); this.registerEvent( this.app.vault.on("delete", (file) => { - console.log("Delete event is fired..."); // Queue the file for processing instead of processing immediately this.queueFileForDeletion(file); }), ); this.registerEvent( this.app.vault.on("create", (file) => { - console.log("Create event is fired..."); if (file instanceof TFile) { // Queue the file for processing instead of processing immediately this.queueFileForCreation(file); @@ -960,30 +1328,39 @@ export default class TaskBoard extends Plugin { }), ); - if ( - this.plugin.settings.data.globalSettings.scanMode !== - scanModeOptions.MANUAL - ) { + if (this.plugin.settings.data.scanMode !== scanModeOptions.MANUAL) { // Listen for editor-blur event and trigger scanning if the editor was modified this.registerEvent( this.app.workspace.on( "active-leaf-change", (leaf: WorkspaceLeaf | null) => { - console.log("On Active Leaf Change...\nLeaf =", leaf); this.onFileModifiedAndLostFocus(); + eventEmitter.emit("SAVE_MAP"); }, ), ); this.registerDomEvent(window, "blur", () => { this.onFileModifiedAndLostFocus(); - console.log("Focusing out of the window..."); + eventEmitter.emit("SAVE_MAP"); }); this.registerDomEvent(window, "focus", () => { - this.onFileModifiedAndLostFocus(); - console.log("Focusing in the window..."); + setTimeout(() => { + this.onFileModifiedAndLostFocus(); + eventEmitter.emit("SAVE_MAP"); + }, 200); }); } + this.registerEvent( + this.app.workspace.on("quit", () => { + const currentTime = getCurrentLocalDateTimeString(); + this.app.saveLocalStorage( + OBSIDIAN_CLOSED_TIME_KEY, + currentTime, + ); + }), + ); + // const closeButton = document.querySelector( // ".titlebar-button.mod-close" // ); @@ -1022,8 +1399,7 @@ export default class TaskBoard extends Plugin { .onClick(() => { if ( fileTypeAllowedForScanning( - this.plugin.settings.data - .globalSettings, + this.plugin.settings.data, file, ) ) { @@ -1034,32 +1410,26 @@ export default class TaskBoard extends Plugin { } }); }); - if ( - this.settings.data.globalSettings.scanFilters.files - .polarity === 2 - ) { + if (this.settings.data.scanFilters.files.polarity === 2) { menu.addItem((item) => { item.setTitle(t("add-file-in-scan-filter")) .setIcon(TaskBoardIcon) .setSection("action") .onClick(() => { - this.settings.data.globalSettings.scanFilters.files.values.push( + this.settings.data.scanFilters.files.values.push( file.path, ); this.saveSettings(); }); }); } - if ( - this.settings.data.globalSettings.scanFilters.files - .polarity === 1 - ) { + if (this.settings.data.scanFilters.files.polarity === 1) { menu.addItem((item) => { item.setTitle(t("add-file-in-scan-filter")) .setIcon(TaskBoardIcon) .setSection("action") .onClick(() => { - this.settings.data.globalSettings.scanFilters.files.values.push( + this.settings.data.scanFilters.files.values.push( file.path, ); this.saveSettings(); @@ -1078,32 +1448,26 @@ export default class TaskBoard extends Plugin { // }); // }); - if ( - this.settings.data.globalSettings.scanFilters.folders - .polarity === 2 - ) { + if (this.settings.data.scanFilters.folders.polarity === 2) { menu.addItem((item) => { item.setTitle(t("add-folder-in-scan-filter")) .setIcon(TaskBoardIcon) .setSection("action") .onClick(() => { - this.settings.data.globalSettings.scanFilters.folders.values.push( + this.settings.data.scanFilters.folders.values.push( file.path, ); this.saveSettings(); }); }); } - if ( - this.settings.data.globalSettings.scanFilters.folders - .polarity === 1 - ) { + if (this.settings.data.scanFilters.folders.polarity === 1) { menu.addItem((item) => { item.setTitle(t("add-folder-in-scan-filter")) .setIcon(TaskBoardIcon) .setSection("action") .onClick(() => { - this.settings.data.globalSettings.scanFilters.folders.values.push( + this.settings.data.scanFilters.folders.values.push( file.path, ); this.saveSettings(); @@ -1165,6 +1529,17 @@ export default class TaskBoard extends Plugin { // } // }) // ); + + const openBoardCallback = (data: { + layout: string; + filePath: string; + duplicate: boolean; + }) => { + this.activateView(data.layout, data.duplicate, data.filePath); + }; + + eventEmitter.on("OPEN_BOARD", openBoardCallback); + return () => eventEmitter.off("OPEN_BOARD", openBoardCallback); } async onFileModifiedAndLostFocus() { @@ -1172,7 +1547,7 @@ export default class TaskBoard extends Plugin { // if (this.currentModifiedFile.path !== this.fileUpdatedUsingModal) { // await this.realTimeScanner.onFileModified( // this.currentModifiedFile, - // this.settings.data.globalSettings.realTimeScanner + // this.settings.data.realTimeScanner // ); // } else { // this.fileUpdatedUsingModal = ""; @@ -1186,254 +1561,62 @@ export default class TaskBoard extends Plugin { // Check if the Tasks plugin is installed and fetch the custom statuses // await fetchTasksPluginCustomStatuses(this.plugin); const tasksPlug = await isTasksPluginEnabled(this.plugin); - this.plugin.settings.data.globalSettings.compatiblePlugins.tasksPlugin = - tasksPlug; + this.plugin.settings.data.compatiblePlugins.tasksPlugin = tasksPlug; // Check if the Reminder plugin is installed isReminderPluginInstalled(this.plugin); } - // private migrateSettings(defaults: any, settings: any) { - // for (const key in defaults) { - // if (!(key in settings)) { - // settings[key] = defaults[key]; - // } else if ( - // // This is a temporary fix for the tagColors - // !Array.isArray(settings[key]) && - // key === "tagColors" && - // typeof settings[key] === "object" && - // settings[key] !== null - // ) { - // settings[key] = Object.entries( - // settings[key] as Record - // ).map( - // ([name, color], idx) => - // ({ - // name, - // color, - // priority: idx + 1, - // } as any) - // ); - // } else if (key === "boardConfigs" && Array.isArray(settings[key])) { - // // This is a temporary solution to sync the boardConfigs. I will need to replace the range object with the new 'datedBasedColumn', which will have three values 'dateType', 'from' and 'to'. So, basically I want to copy range.rangedata.from value to datedBasedColumn.from and similarly for to. And for datedBasedColumn.dateType, put the value this.settings.data.globalSettings.defaultDateType. - // settings[key].forEach((boardConfig: Board) => { - // boardConfig.columns.forEach((column: ColumnData) => { - // if (!column.id) { - // column.id = Math.floor(Math.random() * 1000000); - // } - // if ( - // column.colType === colType.dated || - // (column.colType === colType.undated && - // !column.datedBasedColumn) - // ) { - // column.datedBasedColumn = { - // dateType: - // this.settings.data.globalSettings - // .universalDate, - // from: column.datedBasedColumn?.from || 0, - // to: column.datedBasedColumn?.to || 0, - // }; - // delete column.range; - // } - // }); - - // if (!boardConfig.hideEmptyColumns) { - // boardConfig.hideEmptyColumns = false; - // } - // }); - // } else if ( - // typeof defaults[key] === "object" && - // defaults[key] !== null && - // !Array.isArray(defaults[key]) - // ) { - // // Recursively sync nested objects - // // console.log( - // // "Syncing settings for key:", - // // key, - // // "Defaults:", - // // defaults[key], - // // "Settings:", - // // settings[key] - // // ); - // this.migrateSettings(defaults[key], settings[key]); - // } else if (key === "tasksCacheFilePath" && settings[key] === "") { - // settings[ - // key - // ] = `${this.app.vault.configDir}/plugins/task-board/tasks.json`; - // } - // } - - // this.settings = settings; - // // this.saveSettings(); - // } - - async findModifiedFilesOnAppAbsense() { - if (this.vaultScanner.tasksCache.Modified_at) { - const LAST_UPDATED_TIME = Date.parse( - this.vaultScanner.tasksCache.Modified_at, - ); - console.log( - "Task Board : Fetching all modified files...\nLast modified time :", - LAST_UPDATED_TIME, - ); - let filesScannedCount = 0; - const modifiedCreatedRenamedFiles = this.app.vault - .getFiles() - .filter((file) => { - filesScannedCount++; - return ( - file.stat.mtime > LAST_UPDATED_TIME || - file.stat.ctime > LAST_UPDATED_TIME - ); - }); - - // Find deleted files by comparing cache with current vault files - const currentFilesPaths = new Set( - this.app.vault.getFiles().map((file) => file.path), - ); - const cachedFilesPaths = Object.keys( - this.vaultScanner.tasksCache.Pending || {}, - ).concat(Object.keys(this.vaultScanner.tasksCache.Completed || {})); - const deletedFiles = new Set( - cachedFilesPaths.filter( - (filePath) => !currentFilesPaths.has(filePath), - ), - ); - const deletedFilesList = [...deletedFiles]; - - const changed_files = [...modifiedCreatedRenamedFiles]; - console.log( - "Task Board : Fetching complete.\nModified files :", - changed_files, - "\nDeleted files :", - deletedFilesList, - "\nFiles scanned :", - filesScannedCount, - ); - const totalFilesLength = - changed_files.length + deletedFilesList.length; - - if (totalFilesLength > 0) { - const modifiedFilesNotice = new Notice( - createFragment((f) => { - f.createDiv("bugReportNotice", (el) => { - el.createEl("p", { - text: `Task Board : ${totalFilesLength} files has been modified when Obsidian was inactive.`, - }); - el.createEl("button", { - text: t("show-me"), - cls: "reportBugButton", - onclick: () => { - // el.hide(); - - // Open a modal and show all these file names with their modified date-time in a nice UI. - const modifiedFilesModal = - new ModifiedFilesModal(this.app, { - modifiedFiles: changed_files, - deletedFiles: deletedFilesList, - }); - modifiedFilesModal.open(); - }, - }); - el.createEl("button", { - text: t("scan-them"), - cls: "ignoreBugButton", - onclick: async () => { - modifiedFilesNotice.hide(); - - // Show progress notice - this.currentProgressNotice = new Notice( - `Task Board : Processing modified files: 0/${totalFilesLength}`, - 0, - ); - - this.plugin.vaultScanner - .refreshTasksFromFiles( - changed_files, - false, - ) - .then(async () => { - console.log( - "Task Board : Will now going to update the deleted files cache...", - ); - if (deletedFilesList.length > 0) { - await this.plugin.vaultScanner.deleteCacheForFiles( - deletedFilesList, - ); - console.log( - "Task Board : Completed deleting cache of deleted files...", - ); - } - }); - - let modifiedFilesQueue = changed_files; - - let processed = 0; - while (modifiedFilesQueue.length > 0) { - const file = - modifiedFilesQueue.shift()!; - - try { - processed++; - - // Update progress notice - this.currentProgressNotice.messageEl.textContent = `Task Board : Processing created files: ${processed}/${totalFilesLength}`; - } catch (error) { - console.error( - `Error processing created file ${file.path}:`, - error, - ); - } - - // Add delay between processing each file to prevent blocking UI - if (modifiedFilesQueue.length > 0) { - await new Promise((resolve) => - setTimeout( - resolve, - this.PROCESSING_INTERVAL, - ), - ); - } - } - - // Hide progress notice after completion - this.currentProgressNotice?.hide(); - this.currentProgressNotice = null; - new Notice( - `✓ Task Board : Finished processing ${totalFilesLength} created file(s)`, - ); - }, - }); - }); - }), - 0, - ); - - modifiedFilesNotice.messageEl.onClickEvent((e) => { - if (e.target instanceof HTMLButtonElement) { - e.stopPropagation(); - e.preventDefault(); - e.stopImmediatePropagation(); - } - }); - } - } - } - - private runOnPluginUpdate() { + private async runOnPluginUpdate() { // Check if the plugin version has changed - const currentVersion = newReleaseVersion; // Change this whenever you will going to release a new version. + const currentVersion = CURRENT_PLUGIN_VERSION; // Change this whenever you will going to release a new version. const runMandatoryScan = false; // Change this whenever you will release a major version which requires user to scan the whole vault again. And to enable the notification. const previousVersion = this.settings.version; if (previousVersion == "" || currentVersion !== previousVersion) { - // make the localStorage flag, 'manadatoryScan' to True + // A short custom message to show in Obsidian's Notice on plugin update. + // if (previousVersion !== "") { + // const customMessage = new Notice("", 0); + + // const messageContainer = customMessage.containerEl; + + // const customMessageContainer = messageContainer.createDiv({ + // cls: "taskboardCustomMessageContainer", + // }); + + // customMessageContainer.createEl("h3", { text: "Task Board" }); + // customMessageContainer.createEl("p", { + // text: "Note for existing users", + // cls: "taskboardCustomMessageContainerBold", + // }); + // customMessageContainer.createEl("span", { + // text: "If you were using the custom statuses from Tasks plugin configs. Please import them in Task Board's setting, using a button in the new Custom Statuses setting section. Task Board will no longer import the custom statuses from Tasks plugin automatically.", + // }); + // customMessageContainer.createEl("p", { + // text: "Read the release notes for all the latest features : ", + // }); + // customMessageContainer.createEl("a", { + // text: "Task Board v1.9.4", + // href: `https://github.com/tu2-atmanand/Task-Board/releases/tag/${CURRENT_PLUGIN_VERSION}`, + // }); + // } + + // Show a message to existing users to re-scan the vault on minor version updates + // if (runMandatoryScan && previousVersion === "") { + // const smallMessage = + // "Even being a minor release, this new version of Task Board requires a re-scan of your vault. Kindly re-scan using the top-right button in the task board tab."; + // new Notice(smallMessage, 0); + // } + // This will run only on a fresh plugin install + if (previousVersion === "") { + // creates the DEFAULT_BOARD file if it doesnt exists. + await this.createTemplateBoard(); + } + + // make the localStorage flag, 'manadatoryScan' to True if (previousVersion === "" || runMandatoryScan) { - localStorage.setItem("manadatoryScan", "true"); - const smallMessage = - "Even being a minor release, this new version of Task Board requires a re-scan of your vault. Kindly re-scan using the top-right button in the task board tab."; - new Notice(smallMessage, 0); + localStorage.setItem(MANDATORY_SCAN_KEY, "true"); } this.settings.version = currentVersion; @@ -1452,6 +1635,41 @@ export default class TaskBoard extends Plugin { } } + /** + * This function only runs during the plugin installation time and + * creates the template board(DEFAULT_BOARD) for user to use for the + * first time. + */ + private async createTemplateBoard() { + try { + // Import DEFAULT_BOARDS from BoardConfigs + const DEFAULT_BOARD_REGISTRY_ITEM = Object.values( + DEFAULT_SETTINGS.data.taskBoardFilesRegistry, + )[0]; + + const success = await this.taskBoardFileManager.createNewBoardFile( + DEFAULT_BOARD_REGISTRY_ITEM.filePath, + DEFAULT_BOARD, + ); + + if (success) { + new Notice( + `Task Board: Created the template board file to help you start using the plugin quickly.\n\nBoard Path : ${DEFAULT_BOARD_REGISTRY_ITEM.filePath}`, + 0, + ); + } else { + throw "Task Board: There was an issue while creating the template board file. Please check the logs."; + } + } catch (error) { + bugReporterManagerInsatance.showNotice( + 34, + "Error checking or creating board files", + error as string, + "main.ts/checkAndCreateBoardFiles", + ); + } + } + async fileExists(filePath: string): Promise { return await this.app.vault.adapter.exists(filePath); } diff --git a/manifest.json b/manifest.json index 02757492..66f0a1bd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "task-board", "name": "Task Board", - "version": "1.8.7", + "version": "2.0.0-beta-1", "minAppVersion": "1.4.13", "description": "Manage all your tasks throughout your vault from a single board and much more...", "author": "Atmanand Gauns", diff --git a/migration-log-2026-05-05T19_32_01.log b/migration-log-2026-05-05T19_32_01.log new file mode 100644 index 00000000..3d678baf --- /dev/null +++ b/migration-log-2026-05-05T19_32_01.log @@ -0,0 +1,43 @@ +=== Task Board Migration Log === +Generated: 5/5/2026, 7:32:01 PM + +[7:31:52 PM] [•] Initializing migration process... +[7:31:52 PM] [•] +[7:31:52 PM] [•] [Step 1/4] Creating backup... +[7:31:52 PM] [•] Creating backup of current configuration... +[7:31:53 PM] [✓] ✓ Backup created at the root of the vault with the following name: taskboard-configs-export-2026-05-05T19_31_52.json +[7:31:53 PM] [•] +[7:31:53 PM] [•] [Step 2/4] Creating board files (.taskboard) ... +[7:31:53 PM] [•] The Task Board's board files (.taskboard) will be created at the following path: Meta/Task_Board/Boards/ [All] +[7:31:53 PM] [✓] ✓ Created directory: Meta/Task_Board/Boards +[7:31:53 PM] [•] Processing 4 boards... +[7:31:54 PM] [✓] ✓ Created: Time Based Workflow [Time Based Workflow] +[7:31:54 PM] [✓] ✓ Created: Tag Based Workflow [Tag Based Workflow] +[7:31:55 PM] [✓] ✓ Created: Status Based Workflow [Status Based Workflow] +[7:31:55 PM] [✓] ✓ Created: Release 2.0.0 [Release 2.0.0] +[7:31:55 PM] [•] +[7:31:56 PM] [•] [Step 3/4] Migrating map view data... +[7:31:56 PM] [•] Migrating map view data... +[7:31:56 PM] [✓] ✓ Map view data migrated for following board: Time Based Workflow [Time Based Workflow] +[7:31:57 PM] [✓] ✓ Map view data migrated for following board: Tag Based Workflow [Tag Based Workflow] +[7:31:57 PM] [✓] ✓ Map view data migrated for following board: Status Based Workflow [Status Based Workflow] +[7:31:58 PM] [✓] ✓ Map view data migrated for following board: Release 2.0.0 [Release 2.0.0] +[7:31:58 PM] [•] +[7:31:59 PM] [•] [Step 4/4] Finalizing migration... +[7:31:59 PM] [•] Updating plugin settings... +[7:32:00 PM] [✓] ✓ Plugin settings updated +[7:32:00 PM] [•] +[7:32:00 PM] [✓] ✓ Migration completed successfully! +[7:32:00 PM] [✓] Migrated 4 boards. You can find them at the following path : Meta/Task_Board/Boards/ +[7:32:01 PM] [•] +[7:32:01 PM] [✓] Backup file will be available at the root of the vault: taskboard-configs-export-2026-05-05T19_31_52.json + +=== End of Log === + + + + + + +=== ERRORS === +=== End of ERRORS \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 62b4674c..e4f4f2a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "task-board", - "version": "1.8.7", + "version": "2.0.0-beta-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-board", - "version": "1.8.7", + "version": "2.0.0-beta-1", "license": "GPL-3.0", "dependencies": { "@popperjs/core": "^2.11.8", "@simonwep/pickr": "^1.9.1", "@xyflow/react": "^12.9.3", - "i18next": "^25.5.3", + "date-fns": "^4.1.0", + "i18next": "^25.10.10", "lucide-react": "^0.525.0", "monkey-around": "^3.0.0", "react": "^19.1.0", @@ -29,15 +30,15 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/sortablejs": "^1.15.8", - "@typescript-eslint/eslint-plugin": "8.37.0", - "@typescript-eslint/parser": "8.37.0", - "builtin-modules": "5.0.0", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@typescript-eslint/parser": "^8.37.0", + "builtin-modules": "^5.0.0", "codemirror": "^6.0.0", - "esbuild": "0.25.6", + "esbuild": "^0.25.6", "obsidian": "latest", - "obsidian-typings": "4.43.0", - "tslib": "2.8.1", - "typescript": "^5.8.3" + "obsidian-typings": "obsidian-public-latest", + "tslib": "^2.8.1", + "typescript": "^6.0.3" } }, "node_modules/@antfu/install-pkg": { @@ -55,9 +56,9 @@ } }, "node_modules/@antfu/utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", - "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.0.tgz", + "integrity": "sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==", "dev": true, "license": "MIT", "funding": { @@ -65,18 +66,18 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@braintree/sanitize-url": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", - "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "dev": true, "license": "MIT" }, @@ -102,6 +103,13 @@ "lodash-es": "4.17.21" } }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, "node_modules/@chevrotain/gast": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", @@ -113,6 +121,13 @@ "lodash-es": "4.17.21" } }, + "node_modules/@chevrotain/gast/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, "node_modules/@chevrotain/regexp-to-ast": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", @@ -135,9 +150,9 @@ "license": "Apache-2.0" }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.6", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", - "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", "dev": true, "license": "MIT", "dependencies": { @@ -148,21 +163,31 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", - "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, + "node_modules/@codemirror/commands/node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.11.2", - "resolved": "git+ssh://git@github.com/lishid/cm-language.git#a9c3c7efe17dd1d24395ee2a179fe12dd6ed1e76", + "resolved": "git+ssh://git@github.com/lishid/cm-language.git#2d416d7835867d1b1e8d0e726b147fc1135c9f92", "dev": true, "license": "MIT", "dependencies": { @@ -175,9 +200,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.8.5", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", - "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", "dev": true, "license": "MIT", "dependencies": { @@ -187,14 +212,14 @@ } }, "node_modules/@codemirror/search": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", - "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, @@ -209,9 +234,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "dev": true, "license": "MIT", "dependencies": { @@ -254,9 +279,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -271,9 +296,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -288,9 +313,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -305,9 +330,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -322,9 +347,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -339,9 +364,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -356,9 +381,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -373,9 +398,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -390,9 +415,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -407,9 +432,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -424,9 +449,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -441,9 +466,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -458,9 +483,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -475,9 +500,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -492,9 +517,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -509,9 +534,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -526,9 +551,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -543,9 +568,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -560,9 +585,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -577,9 +602,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -594,9 +619,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -611,9 +636,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -628,9 +653,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -645,9 +670,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -662,9 +687,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -679,9 +704,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -696,9 +721,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -715,9 +740,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -725,62 +750,39 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "minimatch": "^10.2.4" }, "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "peer": true, + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -788,150 +790,74 @@ "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "peer": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "peer": true, + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "peer": true, "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -988,40 +914,37 @@ "mlly": "^1.7.4" } }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "node_modules/@iconify/utils/node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/antfu" } }, "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", "dev": true, "license": "MIT" }, "node_modules/@lezer/highlight": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", - "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "dev": true, "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", "dev": true, "license": "MIT", "dependencies": { @@ -1046,9 +969,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", - "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.99.tgz", + "integrity": "sha512-zN4eQlK3eBf7aJBcTHZilpBH3tDekBzPMIWC8r0s94Ecl73XfOyFi4w7yKFMRVUT0lvNQjtOL8YSrwqQj6mZFg==", "dev": true, "license": "MIT", "optional": true, @@ -1058,23 +981,28 @@ "engines": { "node": ">= 10" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.80", - "@napi-rs/canvas-darwin-arm64": "0.1.80", - "@napi-rs/canvas-darwin-x64": "0.1.80", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", - "@napi-rs/canvas-linux-arm64-musl": "0.1.80", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-musl": "0.1.80", - "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + "@napi-rs/canvas-android-arm64": "0.1.99", + "@napi-rs/canvas-darwin-arm64": "0.1.99", + "@napi-rs/canvas-darwin-x64": "0.1.99", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.99", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.99", + "@napi-rs/canvas-linux-arm64-musl": "0.1.99", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.99", + "@napi-rs/canvas-linux-x64-gnu": "0.1.99", + "@napi-rs/canvas-linux-x64-musl": "0.1.99", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.99", + "@napi-rs/canvas-win32-x64-msvc": "0.1.99" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", - "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.99.tgz", + "integrity": "sha512-9OCRt8VVxA17m32NWZKyNC2qamdaS/SC5CEOIQwFngRq0DIeVm4PDal+6Ljnhqm2whZiC63DNuKZ4xSp2nbj9w==", "cpu": [ "arm64" ], @@ -1086,12 +1014,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", - "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.99.tgz", + "integrity": "sha512-lupMDMy1+H38dhyCcLirOKKVUyzzlxi7j7rGPLI3vViMHOoPjcXO1b10ivy+ad+q6MiwHfoLjKTCoLke5ySOBg==", "cpu": [ "arm64" ], @@ -1103,12 +1035,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", - "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.99.tgz", + "integrity": "sha512-fdz02t4w8n6Ii/rYhWig6STb/zcTmCC/6YZTGmjoDeidDwn9Wf0ukQVynhCPEs29vqUc66wHZKsuIgMs9tycCg==", "cpu": [ "x64" ], @@ -1120,12 +1056,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", - "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.99.tgz", + "integrity": "sha512-w4FwVwlNo00ezeRhfY62IVIyt6G3u8wodkPtiqWc52BUHx+VDBUM2vkS3ogfANaLI7hnf3s6WK4LyZVUjBg1lA==", "cpu": [ "arm" ], @@ -1137,12 +1077,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", - "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.99.tgz", + "integrity": "sha512-8JvHeexKQ8c7g0q7YJ29NVQwnf1ePghP9ys9ZN0R0qzyqJQ9Uw6N9qnDINArlm3IYHexB7LjzArIfhQiqSDGvQ==", "cpu": [ "arm64" ], @@ -1154,12 +1098,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", - "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.99.tgz", + "integrity": "sha512-Z+6nyLdJXWzLPVxi4H6g9TJop4DwN3KSgHWto5JCbZV5/uKoVqcSynPs0tGlUHOoWI8S8tEvJspz51GQkvr07w==", "cpu": [ "arm64" ], @@ -1171,12 +1119,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", - "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.99.tgz", + "integrity": "sha512-jAnfOUv4IO1l8Levk5t85oVtEBOXLa07KnIUgWo1CDlPxiqpxS3uBfiE38Lvj/CQgHaNF6Nxk/SaemwLgsVJgw==", "cpu": [ "riscv64" ], @@ -1188,12 +1140,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", - "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.99.tgz", + "integrity": "sha512-mIkXw3fGmbYyFjSmfWEvty4jN+rwEOmv0+Dy9bRvvTzLYWCgm3RMgUEQVfAKFw96nIRFnyNZiK83KNQaVVFjng==", "cpu": [ "x64" ], @@ -1205,12 +1161,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", - "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.99.tgz", + "integrity": "sha512-f3Uz2P0RgrtBHISxZqr6yiYXJlTDyCVBumDacxo+4AmSg7z0HiqYZKGWC/gszq3fbPhyQUya1W2AEteKxT9Y6A==", "cpu": [ "x64" ], @@ -1222,14 +1182,18 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", - "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.99.tgz", + "integrity": "sha512-XE6KUkfqRsCNejcoRMiMr3RaUeObxNf6y7dut3hrq2rn7PzfRTZgrjF1F/B2C7FcdgqY/vSHWpQeMuNz1vTNHg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", @@ -1239,44 +1203,31 @@ ], "engines": { "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" }, - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.99.tgz", + "integrity": "sha512-plMYGVbc/vmmPF9MtmHbwNk1rL1Aj53vQZt+Gnv1oZn6gmd9jEHHJ0n9Nd2nxa5sKH7TS5IjkCDM6289O0d6PQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "node": ">= 10" }, - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@pixi/accessibility": { @@ -2012,9 +1963,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "dev": true, "license": "MIT", "dependencies": { @@ -2079,6 +2030,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2094,24 +2053,32 @@ "license": "MIT" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "dev": true, "license": "MIT" }, "node_modules/@types/jsdom": { - "version": "27.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", - "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.1.tgz", + "integrity": "sha512-GJq2QE4TAZ5ajSoCasn5DOFm8u1mI3tIFvM5tIq3W5U/RTB6gsHwc6Yhpl91X9VSDOUVblgXmG+2+sSvFQrdlw==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", - "parse5": "^7.0.0" + "parse5": "^7.0.0", + "undici-types": "^7.21.0" } }, + "node_modules/@types/jsdom/node_modules/undici-types": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.25.0.tgz", + "integrity": "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2131,22 +2098,15 @@ } }, "node_modules/@types/node": { - "version": "24.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", - "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.16.0" } }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", @@ -2162,23 +2122,23 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/responselike": { @@ -2192,9 +2152,9 @@ } }, "node_modules/@types/sortablejs": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz", - "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", "dev": true, "license": "MIT" }, @@ -2241,21 +2201,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz", - "integrity": "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/type-utils": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2265,23 +2224,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.37.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.37.0.tgz", - "integrity": "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2291,20 +2250,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz", - "integrity": "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.37.0", - "@typescript-eslint/types": "^8.37.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2314,18 +2273,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz", - "integrity": "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2336,9 +2295,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz", - "integrity": "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", "dev": true, "license": "MIT", "engines": { @@ -2349,21 +2308,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz", - "integrity": "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0", - "@typescript-eslint/utils": "8.37.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2373,14 +2332,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.37.0.tgz", - "integrity": "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", "dev": true, "license": "MIT", "engines": { @@ -2392,22 +2351,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz", - "integrity": "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.37.0", - "@typescript-eslint/tsconfig-utils": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/visitor-keys": "8.37.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2417,20 +2375,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.37.0.tgz", - "integrity": "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.37.0", - "@typescript-eslint/types": "8.37.0", - "@typescript-eslint/typescript-estree": "8.37.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2440,19 +2398,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz", - "integrity": "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.37.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2463,25 +2421,25 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@xyflow/react": { - "version": "12.9.3", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.9.3.tgz", - "integrity": "sha512-PSWoJ8vHiEqSIkLIkge+0eiHWiw4C6dyFDA03VKWJkqbU4A13VlDIVwKqf/Znuysn2GQw/zA61zpHE4rGgax7Q==", + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.73", + "@xyflow/system": "0.0.76", "classcat": "^5.0.3", "zustand": "^4.4.0" }, @@ -2491,9 +2449,9 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.73", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.73.tgz", - "integrity": "sha512-C2ymH2V4mYDkdVSiRx0D7R0s3dvfXiupVBcko6tXP5K4tVdSBMo22/e3V9yRNdn+2HQFv44RFKzwOyCcUUDAVQ==", + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -2508,9 +2466,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2532,9 +2490,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "peer": true, @@ -2549,38 +2507,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "18 || 20 || >=22" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0", - "peer": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -2591,26 +2527,16 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/buffer-crc32": { @@ -2624,9 +2550,9 @@ } }, "node_modules/builtin-modules": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", - "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.1.0.tgz", + "integrity": "sha512-c5JxaDrzwRjq3WyJkI1AGR5xy6Gr6udlt7sQPbl09+3ckB+Zo2qqQ2KhCTBr7Q8dHB43bENGYEk4xddrFH/b7A==", "dev": true, "license": "MIT", "engines": { @@ -2696,35 +2622,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chevrotain": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", @@ -2753,8 +2650,15 @@ "chevrotain": "^11.0.0" } }, - "node_modules/classcat": { - "version": "5.0.5", + "node_modules/chevrotain/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/classcat": { + "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "license": "MIT" @@ -2788,28 +2692,6 @@ "@codemirror/view": "^6.0.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -2827,18 +2709,10 @@ "node": ">= 10" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "dev": true, "license": "MIT" }, @@ -2887,16 +2761,16 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", + "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", "dev": true, "license": "MIT", "engines": { @@ -3162,9 +3036,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "dev": true, "license": "ISC", "engines": { @@ -3425,17 +3299,27 @@ "lodash-es": "^4.17.21" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "dev": true, "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3536,9 +3420,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3554,9 +3438,9 @@ "optional": true }, "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", "dev": true, "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { @@ -3586,9 +3470,9 @@ "license": "ISC" }, "node_modules/electron": { - "version": "37.2.2", - "resolved": "https://registry.npmjs.org/electron/-/electron-37.2.2.tgz", - "integrity": "sha512-qEIUs+3elu7wOfzLEtLz4Innfe+8nQyhLh9qjrJY0d0MxdRolLMDUD9QwAQ743vDZ+bUg+gqwigzP7kV78S3hA==", + "version": "39.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz", + "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3605,15 +3489,22 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "22.16.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.4.tgz", - "integrity": "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==", + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3689,9 +3580,9 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3702,32 +3593,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escape-string-regexp": { @@ -3744,35 +3635,31 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -3782,8 +3669,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3791,7 +3677,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -3806,18 +3692,20 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3836,27 +3724,15 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "peer": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3873,57 +3749,43 @@ "node": ">= 4" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "peer": true, "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "peer": true, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "peer": true, @@ -3978,9 +3840,9 @@ "license": "MIT" }, "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true, "license": "MIT" }, @@ -4013,36 +3875,6 @@ "license": "MIT", "peer": true }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4059,16 +3891,6 @@ "license": "MIT", "peer": true }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -4079,6 +3901,24 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4093,19 +3933,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4140,9 +3967,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC", "peer": true @@ -4261,12 +4088,11 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -4338,13 +4164,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/hachure-fill": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", @@ -4352,17 +4171,6 @@ "dev": true, "license": "MIT" }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -4391,9 +4199,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -4425,29 +4233,29 @@ } }, "node_modules/i18next": { - "version": "25.5.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz", - "integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==", + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", "funding": [ { "type": "individual", - "url": "https://locize.com" + "url": "https://www.locize.com/i18next" }, { "type": "individual", - "url": "https://locize.com/i18next.html" + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" }, { "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + "url": "https://www.locize.com" } ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6" + "@babel/runtime": "^7.29.2" }, "peerDependencies": { - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "typescript": { @@ -4478,24 +4286,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4523,6 +4313,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4533,6 +4324,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4540,16 +4332,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4565,20 +4347,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4621,9 +4389,9 @@ } }, "node_modules/katex": { - "version": "0.16.25", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz", - "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==", + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", "dev": true, "funding": [ "https://opencollective.com/katex", @@ -4745,20 +4513,12 @@ } }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -4815,16 +4575,6 @@ "node": ">= 0.4" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/mermaid": { "version": "11.4.1", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz", @@ -4854,20 +4604,6 @@ "uuid": "^9.0.1" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -4879,32 +4615,32 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "ufo": "^1.6.3" } }, "node_modules/mlly/node_modules/confbox": { @@ -5000,9 +4736,9 @@ } }, "node_modules/obsidian": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.0.tgz", - "integrity": "sha512-F7hhnmGRQD1TanDPFT//LD3iKNUVd7N8sKL7flCCHRszfTxpDJ39j3T7LHbcGpyid906i6lD5oO+cnfLBzJMKw==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5011,13 +4747,13 @@ }, "peerDependencies": { "@codemirror/state": "6.5.0", - "@codemirror/view": "6.38.1" + "@codemirror/view": "6.38.6" } }, "node_modules/obsidian-typings": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/obsidian-typings/-/obsidian-typings-4.43.0.tgz", - "integrity": "sha512-3sC9QNrSxkVqB/YivPsmHoMKxPHVDKSx45xQEcAZ1SMMX/11rXPe9UdgQsDFjBGFRTFie/QHiTmZVMXbHPSEDw==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/obsidian-typings/-/obsidian-typings-5.21.0.tgz", + "integrity": "sha512-Ftnd4CXFIjiiZZ7JsqIeNmVXiq8O8G6wAD1VElhRBM+xh2CKhfc7fzvKIfn0y50Xws4tqxxyceZWzazabL+hdA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5026,60 +4762,60 @@ "@capacitor/core": "5.7.8", "@codemirror/language": "6.11.2", "@codemirror/search": "6.5.11", - "@codemirror/state": "6.5.2", + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6", "@lezer/common": "1.2.3", "@pixi/color": "7.2.4", "@pixi/events": "7.2.4", "@pixi/settings": "7.2.4", - "@types/codemirror": "5.60.16", + "@types/codemirror": "5.60.8", "@types/css-font-loading-module": "0.0.14", "@types/dompurify": "3.0.1", - "@types/node": "^18.17.0 || >=20.1.0", + "@types/node": "25.0.3", "@types/prismjs": "1.26.5", "@types/turndown": "5.0.5", "colord": "2.9.3", - "electron": ">=1.6.10", + "electron": "39.2.7", "i18next": "25.2.1", "mermaid": "11.4.1", - "moment": "2.29.4", - "obsidian": "1.8.7", + "obsidian": "1.12.3", "pdfjs-dist": "5.3.31", "pixi.js": "7.2.4", "scrypt-js": "3.0.1", - "style-mod": "4.1.2" + "style-mod": "4.1.3", + "type-fest": "5.3.1" }, "peerDependencies": { "typescript": ">=4.8.0" } }, - "node_modules/obsidian-typings/node_modules/@antfu/utils": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.0.tgz", - "integrity": "sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==", + "node_modules/obsidian-typings/node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" } }, - "node_modules/obsidian-typings/node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "node_modules/obsidian-typings/node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", "dev": true, - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } + "license": "MIT" }, - "node_modules/obsidian-typings/node_modules/@types/codemirror": { - "version": "5.60.16", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", - "integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==", + "node_modules/obsidian-typings/node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", "dependencies": { - "@types/tern": "*" + "undici-types": "~7.16.0" } }, "node_modules/obsidian-typings/node_modules/i18next": { @@ -5114,41 +4850,6 @@ } } }, - "node_modules/obsidian-typings/node_modules/obsidian": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", - "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/codemirror": "5.60.8", - "moment": "2.29.4" - }, - "peerDependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/obsidian-typings/node_modules/obsidian/node_modules/@types/codemirror": { - "version": "5.60.8", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", - "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/tern": "*" - } - }, - "node_modules/obsidian-typings/node_modules/obsidian/node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5223,26 +4924,12 @@ } }, "node_modules/package-manager-detector": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.4.1.tgz", - "integrity": "sha512-dSMiVLBEA4XaNJ0PRb4N5cV/SEP4BWrWZKBmfF+OUm2pQTiZ6DDkKeWaltwu3JRhLoy59ayIkJ00cx9K9CaYTg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "dev": true, "license": "MIT" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -5313,13 +5000,13 @@ "license": "MIT" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -5420,9 +5107,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -5442,9 +5129,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5474,27 +5161,6 @@ ], "license": "MIT" }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -5509,30 +5175,30 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.2.5" } }, "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", "license": "MIT", "peerDependencies": { "react": "*" @@ -5545,17 +5211,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -5569,17 +5224,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -5600,9 +5244,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "dev": true, "license": "Unlicense" }, @@ -5619,30 +5263,6 @@ "points-on-path": "^0.2.1" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -5658,9 +5278,9 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/scrypt-js": { @@ -5671,9 +5291,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5768,14 +5388,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -5824,9 +5444,9 @@ } }, "node_modules/sortablejs": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", - "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", "license": "MIT" }, "node_modules/sprintf-js": { @@ -5837,31 +5457,17 @@ "license": "BSD-3-Clause", "optional": true }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, "license": "MIT" }, "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", "dev": true, "license": "MIT" }, @@ -5878,44 +5484,50 @@ "node": ">= 8.0" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -5956,10 +5568,26 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", + "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -5971,16 +5599,16 @@ } }, "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "dev": true, "license": "MIT" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -6027,9 +5655,9 @@ "license": "MIT" }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index 4e05ed16..da5288e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "task-board", - "version": "1.8.7", + "version": "2.0.0-beta-1", "description": "An Obsidian plugin to manage small to large projects using tasks from the whole vault on a centralized board using various kinds of views like Kanban, map, list, etc.", "main": "main.js", "type": "module", @@ -21,21 +21,22 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/sortablejs": "^1.15.8", - "@typescript-eslint/eslint-plugin": "8.37.0", - "@typescript-eslint/parser": "8.37.0", - "builtin-modules": "5.0.0", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@typescript-eslint/parser": "^8.37.0", + "builtin-modules": "^5.0.0", "codemirror": "^6.0.0", - "esbuild": "0.25.6", + "esbuild": "^0.25.6", "obsidian": "latest", - "obsidian-typings": "4.43.0", - "tslib": "2.8.1", - "typescript": "^5.8.3" + "obsidian-typings": "obsidian-public-latest", + "tslib": "^2.8.1", + "typescript": "^6.0.3" }, "dependencies": { "@popperjs/core": "^2.11.8", "@simonwep/pickr": "^1.9.1", "@xyflow/react": "^12.9.3", - "i18next": "^25.5.3", + "date-fns": "^4.1.0", + "i18next": "^25.10.10", "lucide-react": "^0.525.0", "monkey-around": "^3.0.0", "react": "^19.1.0", diff --git a/src/components/AddOrEditTaskRC.tsx b/src/components/AddOrEditTaskRC.tsx index 1bde4df5..170a7559 100644 --- a/src/components/AddOrEditTaskRC.tsx +++ b/src/components/AddOrEditTaskRC.tsx @@ -5,31 +5,31 @@ import { Component, Keymap, Notice, Platform, TFile, UserEvent, debounce, normal import { FaTimes, FaTrash } from 'react-icons/fa'; import React, { useEffect, useRef, useState } from "react"; import Sortable from "sortablejs"; -import { cursorLocation, taskItem } from "src/interfaces/TaskItem"; -import { moment as _moment } from "obsidian"; -import TaskBoard from "main"; -import { updateRGBAOpacity } from "src/utils/UIHelpers"; -import { t } from "src/utils/lang/helper"; -import { cleanTaskTitleLegacy, getFormattedTaskContentSync, sanitizeCancelledDate, sanitizeCompletionDate, sanitizeCreatedDate, sanitizeDependsOn, sanitizeDueDate, sanitizePriority, sanitizeReminder, sanitizeScheduledDate, sanitizeStartDate, sanitizeStatus, sanitizeTags, sanitizeTime } from "src/utils/taskLine/TaskContentFormatter"; -import { buildTaskFromRawContent } from "src/managers/VaultScanner"; -import { DeleteIcon, EditIcon, FileInput, Network, PanelRightOpenIcon, RefreshCcw } from "lucide-react"; -import { MultiSuggest, getFileSuggestions, getPendingTasksSuggestions, getQuickAddPluginChoices, getTagSuggestions } from "src/services/MultiSuggest"; -import { CommunityPlugins } from "src/services/CommunityPlugins"; -import { bugReporter, openEditTaskView } from "src/services/OpenModals"; -import { MarkdownUIRenderer } from "src/services/MarkdownUIRenderer"; -import { getObsidianIndentationSetting, isTaskLine } from "src/utils/CheckBoxUtils"; -import { formatTaskNoteContent, isTaskNotePresentInTags } from "src/utils/taskNote/TaskNoteUtils"; -import { eventEmitter } from "src/services/EventEmitter"; -import { allowedFileExtensionsRegEx } from "src/regularExpressions/MiscelleneousRegExpr"; -import { markdownButtonHoverPreviewEvent } from "src/services/MarkdownHoverPreview"; +import { DeleteIcon, EditIcon, FileInput, Network, PanelRightOpenIcon } from "lucide-react"; import { ViewUpdate } from "@codemirror/view"; -import { createEmbeddableMarkdownEditor, EmbeddableMarkdownEditor } from "src/services/MarkdownEditor"; -import { UniversalDateOptions, EditButtonMode, NotificationService, statusTypeNames, onCompletionOptions } from "src/interfaces/Enums"; -import { getPriorityOptionsForDropdown, taskItemEmpty } from "src/interfaces/Mapping"; -import { applyIdToTaskItem, getTaskFromId } from "src/utils/TaskItemUtils"; -import { handleEditTask } from "src/utils/UserTaskEvents"; import { RxDragHandleHorizontal } from "react-icons/rx"; -import { bugReporterManagerInsatance } from "src/managers/BugReporter"; +import TaskBoard from "../../main.js"; +import { statusTypeNames, UniversalDateOptions, viewTypeNames, EditButtonMode, NotificationService } from "../interfaces/Enums.js"; +import { taskItemEmpty, getPriorityOptionsForDropdown } from "../interfaces/Mapping.js"; +import { taskItem, cursorLocationInterface } from "../interfaces/TaskItem.js"; +import { bugReporterManagerInsatance } from "../managers/BugReporter.js"; +import { buildTaskFromRawContent } from "../managers/VaultScanner.js"; +import { allowedFileExtensionsRegEx } from "../regularExpressions/MiscelleneousRegExpr.js"; +import { CommunityPlugins } from "../services/CommunityPlugins.js"; +import { eventEmitter } from "../services/EventEmitter.js"; +import { EmbeddableMarkdownEditor, createEmbeddableMarkdownEditor } from "../services/MarkdownEditor.js"; +import { markdownButtonHoverPreviewEvent } from "../services/MarkdownHoverPreview.js"; +import { MarkdownUIRenderer } from "../services/MarkdownUIRenderer.js"; +import { getTagSuggestions, MultiSuggest, getQuickAddPluginChoices, getFileSuggestions, getPendingTasksSuggestions } from "../services/MultiSuggest.js"; +import { openEditTaskView } from "../services/OpenModals.js"; +import { verifySubtasksAndChildtasksAreComplete } from "../utils/algorithms/ScanningFilterer.js"; +import { getObsidianIndentationSetting, isTaskLine } from "../utils/CheckBoxUtils.js"; +import { applyIdToTaskItem, getTaskFromId } from "../utils/TaskItemUtils.js"; +import { getFormattedTaskContentSync, cleanTaskTitleLegacy, sanitizeStatus, sanitizeCreatedDate, sanitizeStartDate, sanitizeScheduledDate, sanitizeDueDate, sanitizeReminder, sanitizePriority, sanitizeTime, sanitizeTags, sanitizeDependsOn } from "../utils/taskLine/TaskContentFormatter.js"; +import { formatTaskNoteContent, isTaskNotePresentInTags } from "../utils/taskNote/TaskNoteUtils.js"; +import { updateRGBAOpacity } from "../utils/UIHelpers.js"; +import { handleEditTask } from "../utils/UserTaskEvents.js"; +import { t } from "../utils/lang/helper.js"; export interface filterOptions { value: string; @@ -50,6 +50,9 @@ export const AddOrEditTaskRC: React.FC<{ onClose: () => void; setIsEdited: (value: boolean) => void; }> = ({ plugin, root, isTaskNote, noteContent, task = taskItemEmpty, taskExists, activeNote, filePath, onSave, onClose, setIsEdited }) => { + const globalSettings = plugin.settings.data; + + // All useState const [title, setTitle] = useState( task.title ? task.title @@ -65,7 +68,7 @@ export const AddOrEditTaskRC: React.FC<{ const [endTime, setEndTime] = useState(task.time ? task?.time?.split('-')[1]?.trim() || '' : ""); const [newTime, setNewTime] = useState(task.time || ''); const [priority, setPriority] = useState(task.priority || 0); - const [status, setStatus] = useState(task.status || ''); + const [status, setStatus] = useState(task.status || ' '); const [reminder, setReminder] = useState(task?.reminder || ""); const [dependsOn, setDependsOn] = useState(task?.dependsOn || []); const [childTasks, setChildTasks] = useState([]); @@ -73,11 +76,11 @@ export const AddOrEditTaskRC: React.FC<{ const [formattedTaskContent, setFormattedTaskContent] = useState(isTaskNote ? noteContent : getFormattedTaskContentSync(task)); const frontmatterContentRef = useRef(''); const [newFilePath, setNewFilePath] = useState(filePath); - const [quickAddPluginChoice, setQuickAddPluginChoice] = useState(plugin.settings.data.globalSettings.quickAddPluginDefaultChoice || ''); + const [quickAddPluginChoice, setQuickAddPluginChoice] = useState(globalSettings.quickAddPluginDefaultChoice || ''); const [markdownEditor, setMarkdownEditor] = useState(null); const [isEditorContentChanged, setIsEditorContentChanged] = useState(true); - const cursorLocationRef = useRef(null); + const cursorLocationRef = useRef(null); const indentationString = getObsidianIndentationSetting(plugin); @@ -104,13 +107,17 @@ export const AddOrEditTaskRC: React.FC<{ let filteredStatusesDropdown: filterOptions[] = []; // Fetch all the custom statuses and add them to the dropdown - if (plugin.settings.data.globalSettings.customStatuses?.length > 0) { - filteredStatusesDropdown = plugin.settings.data.globalSettings.customStatuses.map((customStatus) => ({ + if (plugin.settings.data.customStatuses?.length > 0) { + filteredStatusesDropdown = plugin.settings.data.customStatuses.map((customStatus) => ({ value: customStatus.symbol, text: `${customStatus.name} [${customStatus.symbol}]`, })); } else { - console.error("No custom statuses found."); + bugReporterManagerInsatance.addToLogs( + 129, + `customStatuses are empty in the settings.`, + "AddOrEditTaskRC.tsx", + ); } // ------------ Handle task property values changes ------------ @@ -155,20 +162,29 @@ export const AddOrEditTaskRC: React.FC<{ // setIsEdited(true); // }; - const handleStatusChange = (symbol: string) => { - setStatus(symbol); - setIsEdited(true); - + const handleStatusChange = async (symbol: string) => { const statusConfig = - plugin.settings.data.globalSettings.customStatuses.find( + globalSettings.customStatuses.find( (status) => status.symbol === symbol ); const statusType = statusConfig ? statusConfig.type : statusTypeNames.TODO; + + if (statusType === statusTypeNames.DONE) { + const allowed = await verifySubtasksAndChildtasksAreComplete(plugin, task); + + if (!allowed) { + new Notice(t("verifySubtasksAndChildtasksAreComplete-false-message")); + return; + } + } + + setStatus(symbol); + // if (statusType === statusTypeNames.DONE) { // const globalSettings = plugin.settings.data.globalSettings; // const moment = _moment as unknown as typeof _moment.default; // const currentDateValue = moment().format( - // globalSettings?.taskCompletionDateTimePattern + // globalSettings?.dateTimeFormat // ); // const newTitle = sanitizeCompletionDate( // globalSettings, @@ -180,7 +196,7 @@ export const AddOrEditTaskRC: React.FC<{ // const globalSettings = plugin.settings.data.globalSettings; // const moment = _moment as unknown as typeof _moment.default; // const currentDateValue = moment().format( - // globalSettings?.taskCompletionDateTimePattern + // globalSettings?.dateTimeFormat // ); // const newTitle = sanitizeCancelledDate( // globalSettings, @@ -196,10 +212,12 @@ export const AddOrEditTaskRC: React.FC<{ // setTitle(newTitle); // } - const globalSettings = plugin.settings.data.globalSettings; - const newTitle = sanitizeStatus(globalSettings, task.title, symbol, statusType); - setTitle(newTitle); + if (!isTaskNote) { + const newTitle = sanitizeStatus(globalSettings, title, symbol, statusType); + setTitle(newTitle); + } + setIsEdited(true); setIsEditorContentChanged(true); } @@ -207,7 +225,7 @@ export const AddOrEditTaskRC: React.FC<{ setCreatedDate(value); if (!isTaskNote) { - const newTitle = sanitizeCreatedDate(plugin.settings.data.globalSettings, title, value, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeCreatedDate(plugin.settings.data, title, value, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -219,7 +237,7 @@ export const AddOrEditTaskRC: React.FC<{ setStartDate(value); if (!isTaskNote) { - const newTitle = sanitizeStartDate(plugin.settings.data.globalSettings, title, value, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeStartDate(plugin.settings.data, title, value, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -231,7 +249,7 @@ export const AddOrEditTaskRC: React.FC<{ setScheduledDate(value); if (!isTaskNote) { - const newTitle = sanitizeScheduledDate(plugin.settings.data.globalSettings, title, value, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeScheduledDate(plugin.settings.data, title, value, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -243,7 +261,7 @@ export const AddOrEditTaskRC: React.FC<{ setDue(value); if (!isTaskNote) { - const newTitle = sanitizeDueDate(plugin.settings.data.globalSettings, title, value, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeDueDate(plugin.settings.data, title, value, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -260,7 +278,7 @@ export const AddOrEditTaskRC: React.FC<{ // setTitle(title.replace(reminderRegex, "")); // } if (!isTaskNote) { - const newTitle = sanitizeReminder(plugin.settings.data.globalSettings, title, value, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeReminder(plugin.settings.data, title, value, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -272,7 +290,7 @@ export const AddOrEditTaskRC: React.FC<{ setPriority(value); if (!isTaskNote) { - const newTitle = sanitizePriority(plugin.settings.data.globalSettings, title, value, cursorLocationRef.current ?? undefined); + const newTitle = sanitizePriority(plugin.settings.data, title, value, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -299,7 +317,7 @@ export const AddOrEditTaskRC: React.FC<{ } if (!isTaskNote) { - const newTitle = sanitizeTime(plugin.settings.data.globalSettings, title, newTime, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeTime(plugin.settings.data, title, newTime, cursorLocationRef.current ?? undefined); setTitle(newTitle); } setIsEditorContentChanged(true); @@ -377,7 +395,7 @@ export const AddOrEditTaskRC: React.FC<{ const newTagsList = tags.concat(input); if (!isTaskNote) { - const newTitle = sanitizeTags(title, tags, newTagsList, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeTags(title, newTagsList, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -409,7 +427,7 @@ export const AddOrEditTaskRC: React.FC<{ if (!isTaskNote) { const newTagsList = currentTags.concat(choice); - const newTitle = sanitizeTags(currentTitle, currentTags, newTagsList, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeTags(currentTitle, newTagsList, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -433,7 +451,7 @@ export const AddOrEditTaskRC: React.FC<{ const newTags = tags.filter(tag => tag !== tagToRemove); if (!isTaskNote) { - const newTitle = sanitizeTags(title, tags, newTags, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeTags(title, newTags, cursorLocationRef.current ?? undefined); setTitle(newTitle); } setTags(newTags); @@ -459,35 +477,44 @@ export const AddOrEditTaskRC: React.FC<{ }; const handleSaveAsTaskLine = () => { - let newDue = due; + let newDueDate = due; let newStartDate = startDate; let newScheduledDate = scheduledDate; + let newTitle = title; - if (plugin.settings.data.globalSettings.autoAddUniversalDate && !taskExists) { - const universalDateType = plugin.settings.data.globalSettings.universalDate; + if (plugin.settings.data.autoAddUniversalDate && !taskExists) { + const universalDateType = plugin.settings.data.universalDate; if (universalDateType === UniversalDateOptions.dueDate && !due) { - newDue = new Date().toISOString().split('T')[0]; + newDueDate = new Date().toISOString().split('T')[0]; + newTitle = sanitizeDueDate(globalSettings, newTitle, newDueDate); } else if (universalDateType === UniversalDateOptions.startDate && !startDate) { newStartDate = new Date().toISOString().split('T')[0]; + newTitle = sanitizeStartDate(globalSettings, newTitle, newStartDate); } else if (universalDateType === UniversalDateOptions.scheduledDate && !scheduledDate) { newScheduledDate = new Date().toISOString().split('T')[0]; + newTitle = sanitizeScheduledDate(globalSettings, newTitle, newScheduledDate); } } let newCreatedDate = createdDate; - if (plugin.settings.data.globalSettings.autoAddCreatedDate && !taskExists) { + if (globalSettings.autoAddCreatedDate && !taskExists) { newCreatedDate = new Date().toISOString().split('T')[0]; + newTitle = sanitizeCreatedDate(globalSettings, newTitle, newCreatedDate); } + + let editedFilePath = allowedFileExtensionsRegEx.test(newFilePath) ? newFilePath : `${newFilePath}.md`; + editedFilePath = normalizePath(editedFilePath); + const updatedTask = { ...task, - title, + title: newTitle, body: [ ...bodyContent.split('\n'), ], createdDate: newCreatedDate, startDate: newStartDate, scheduledDate: newScheduledDate, - due: newDue, + due: newDueDate, tags, time: newTime, priority, @@ -508,8 +535,8 @@ export const AddOrEditTaskRC: React.FC<{ let newStartDate = startDate; let newScheduledDate = scheduledDate; - if (plugin.settings.data.globalSettings.autoAddUniversalDate && !taskExists) { - const universalDateType = plugin.settings.data.globalSettings.universalDate; + if (globalSettings.autoAddUniversalDate && !taskExists) { + const universalDateType = globalSettings.universalDate; if (universalDateType === UniversalDateOptions.dueDate && !due) { newDue = new Date().toISOString().split('T')[0]; } else if (universalDateType === UniversalDateOptions.startDate && !startDate) { @@ -520,7 +547,7 @@ export const AddOrEditTaskRC: React.FC<{ } let newCreatedDate = createdDate; - if (plugin.settings.data.globalSettings.autoAddCreatedDate && !taskExists) { + if (globalSettings.autoAddCreatedDate && !taskExists) { newCreatedDate = new Date().toISOString().split('T')[0]; } @@ -530,7 +557,7 @@ export const AddOrEditTaskRC: React.FC<{ const taskNoteItem: taskItem = { ...modifiedTask, - title: title, + title: title === "" ? taskNoteFilePath.split('/').pop() ?? "No title" : title, body: formattedTaskContent ? formattedTaskContent.split('\n').filter(line => isTaskLine(line)) : [], createdDate: newCreatedDate, startDate: newStartDate, @@ -547,8 +574,10 @@ export const AddOrEditTaskRC: React.FC<{ dependsOn: dependsOn, }; + const newFormattedNoteContent = formatTaskNoteContent(plugin, taskNoteItem, formattedTaskContent); + // Call onSave with the task note item - onSave(taskNoteItem, quickAddPluginChoice, formattedTaskContent ? formattedTaskContent : undefined); + onSave(taskNoteItem, quickAddPluginChoice, newFormattedNoteContent.newContent ? newFormattedNoteContent.newContent : undefined); }; let modifiedTask: taskItem = { @@ -567,7 +596,7 @@ export const AddOrEditTaskRC: React.FC<{ filePath: newFilePath, status: status, reminder: reminder, - taskLocation: task.taskLocation, + taskLocation: task?.taskLocation || taskItemEmpty.taskLocation, dependsOn: dependsOn, completion: task.completion || '', cancelledDate: task.cancelledDate || '', @@ -607,21 +636,20 @@ export const AddOrEditTaskRC: React.FC<{ }, [plugin.app]); const handleOpenTaskInMapView = () => { - // if (!plugin.settings.data.globalSettings.experimentalFeatures) { + // if (!globalSettings.experimentalFeatures) { // new Notice(t("enable-experimental-features-message")); // return; // } applyIdToTaskItem(plugin, task).then((newId) => { - plugin.settings.data.globalSettings.lastViewHistory.viewedType = 'map'; - plugin.settings.data.globalSettings.lastViewHistory.taskId = newId ? String(newId) : (task.legacyId ? task.legacyId : String(plugin.settings.data.globalSettings.uniqueIdCounter)); + globalSettings.lastViewHistory.taskId = newId ? String(newId) : (task.legacyId ? task.legacyId : String(globalSettings.uniqueIdCounter)); // console.log("Preparing to open task in kanban view. Current file path:", newFilePath, "\nTask ID:", task.id, "\nLegacy ID:", task.legacyId, "\nnewId:", newId); plugin.realTimeScanner.processAllUpdatedFiles(filePath).then(() => { onClose(); sleep(1000).then(() => { - eventEmitter.emit("SWITCH_VIEW", 'map'); + eventEmitter.emit("SWITCH_VIEW", viewTypeNames.map); }); }); }); @@ -637,7 +665,7 @@ export const AddOrEditTaskRC: React.FC<{ const [isCtrlPressed, setIsCtrlPressed] = useState(false); useEffect(() => { - if (!Platform.isMobile) { + if (!Platform.isMobile && !isTaskNote) { markdownEditor?.editor?.focus(); } const handleKeyDown = (e: KeyboardEvent) => { @@ -717,7 +745,7 @@ export const AddOrEditTaskRC: React.FC<{ setEndTime(updatedTask.time ? updatedTask.time?.split('-')[1]?.trim() || '' : ""); setNewTime(updatedTask.time ? updatedTask.time : ""); setPriority(updatedTask.priority || 0); - setStatus(updatedTask.status || ''); + setStatus(updatedTask.status || ' '); setReminder(updatedTask.reminder || ''); }, 50); @@ -972,7 +1000,7 @@ export const AddOrEditTaskRC: React.FC<{ if (!prev.includes(task.legacyId ? task.legacyId : task.id)) { if (newId === undefined && !selectedTask?.legacyId) { bugReporterManagerInsatance.showNotice(24, "Both newId and legacyId are undefined", `Both newId and legacyId are undefined for the selected task titled ${selectedTask.title}.`, "AddOrEditTaskModal.tsx/EditTaskContent/childTaskInputRef useEffect/getUpdatedDependsOnIds"); - return [...prev, String(plugin.settings.data.globalSettings.uniqueIdCounter)]; + return [...prev, String(globalSettings.uniqueIdCounter)]; } else if (newId === undefined) { return [...prev, selectedTask.legacyId]; } else if (newId) { @@ -986,11 +1014,11 @@ export const AddOrEditTaskRC: React.FC<{ setDependsOn(prev => { const updated = getUpdatedDependsOnIds(prev); if (!isTaskNote) { - const newTitle = sanitizeDependsOn(plugin.settings.data.globalSettings, title, updated, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeDependsOn(plugin.settings.data, title, updated, cursorLocationRef.current ?? undefined); setTitle(newTitle); } - selectedTask.legacyId = selectedTask.legacyId ? selectedTask.legacyId : (newId ? String(newId) : String(plugin.settings.data.globalSettings.uniqueIdCounter)); + selectedTask.legacyId = selectedTask.legacyId ? selectedTask.legacyId : (newId ? String(newId) : String(globalSettings.uniqueIdCounter)); setChildTasks(prevChildTasks => { // Avoid adding duplicates if (!prevChildTasks.find(t => t.id === selectedTask.id)) { @@ -1020,8 +1048,12 @@ export const AddOrEditTaskRC: React.FC<{ const validTasks = tasks.filter(Boolean) as taskItem[]; setChildTasks(validTasks); }) - .catch(err => { - console.error("Error fetching child tasks:", err); + .catch((err) => { + bugReporterManagerInsatance.addToLogs( + 130, + String(err), + "AddOrEditTaskRC.tsx/fetching child-tasks useEffect", + ); }); } }, []); @@ -1061,7 +1093,7 @@ export const AddOrEditTaskRC: React.FC<{ // // Clear existing children in the leaf // await leaf.open(new AddOrEditTaskModal(plugin, childTask, onSave, onClose, true, activeNote)); - const settingOption = plugin.settings.data.globalSettings.editButtonAction; + const settingOption = globalSettings.editButtonAction; switch (settingOption) { case EditButtonMode.NoteInSplit: case EditButtonMode.NoteInTab: @@ -1074,10 +1106,10 @@ export const AddOrEditTaskRC: React.FC<{ event.ctrlKey = false; break; case EditButtonMode.Modal: - case EditButtonMode.View: + case EditButtonMode.ViewInWindow: case EditButtonMode.TasksPluginModal: default: - const isTaskNotePresent = isTaskNotePresentInTags(plugin.settings.data.globalSettings.taskNoteIdentifierTag, childTask.tags); + const isTaskNotePresent = isTaskNotePresentInTags(globalSettings.taskNoteIdentifierTag, childTask.tags); openEditTaskView(plugin, isTaskNotePresent, false, true, childTask, childTask.filePath, "window"); break; } @@ -1110,7 +1142,7 @@ export const AddOrEditTaskRC: React.FC<{ const newDependsOn = dependsOn.filter(id => id !== taskId); setDependsOn(newDependsOn); if (!isTaskNote) { - const newTitle = sanitizeDependsOn(plugin.settings.data.globalSettings, title, newDependsOn, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeDependsOn(plugin.settings.data, title, newDependsOn, cursorLocationRef.current ?? undefined); setTitle(newTitle); } setIsEdited(true); @@ -1133,7 +1165,6 @@ export const AddOrEditTaskRC: React.FC<{ easing: "cubic-bezier(1, 0, 0, 1)", onSort: (evt) => { try { - console.log("Lets go..."); if (evt.oldIndex === undefined || evt.newIndex === undefined) return; // Reorder the dependsOn array based on the drag and drop @@ -1145,7 +1176,7 @@ export const AddOrEditTaskRC: React.FC<{ // Update the title with the new dependsOn order if not a task note if (!isTaskNote) { - const newTitle = sanitizeDependsOn(plugin.settings.data.globalSettings, title, updatedDependsOn, cursorLocationRef.current ?? undefined); + const newTitle = sanitizeDependsOn(plugin.settings.data, title, updatedDependsOn, cursorLocationRef.current ?? undefined); setTitle(newTitle); } @@ -1179,6 +1210,7 @@ export const AddOrEditTaskRC: React.FC<{ value={title} onChange={e => handleTaskTitleChange(e.target.value)} placeholder={t("task-note-title-placeholder")} + autoFocus={taskExists ? false : true} /> )} @@ -1197,10 +1229,10 @@ export const AddOrEditTaskRC: React.FC<{ */}
-
{(communityPlugins.isQuickAddPluginIntegrationEnabled() && !taskExists && !isTaskNote && !activeNote) ? t("quickadd-plugin-choice") : t("file")} +
@@ -1345,7 +1377,7 @@ export const AddOrEditTaskRC: React.FC<{
{/* Task Created Date */} - {!plugin.settings.data.globalSettings.autoAddCreatedDate && + {!globalSettings.autoAddCreatedDate &&
handleCreatedDateChange(e.target.value)} /> @@ -1371,7 +1403,7 @@ export const AddOrEditTaskRC: React.FC<{
{/* Task reminder date-time selector */} - {plugin.settings.data.globalSettings.notificationService !== NotificationService.None && ( + {globalSettings.notificationService !== NotificationService.None && (
{tags.map((tag: string) => { const tagName = tag.replace('#', ''); - const customTagData = plugin.settings.data.globalSettings.tagColors.find(t => t.name === tagName); + const customTagData = globalSettings.tagColors.find(t => t.name === tagName); const tagColor = customTagData?.color; - const backgroundColor = tagColor ? updateRGBAOpacity(plugin, tagColor, 0.1) : `var(--tag-background)`; - const borderColor = tagColor ? updateRGBAOpacity(plugin, tagColor, 0.5) : `var(--tag-color-hover)`; + const backgroundColor = tagColor ? updateRGBAOpacity(tagColor, 0.1) : `var(--tag-background)`; + const borderColor = tagColor ? updateRGBAOpacity(tagColor, 0.5) : `var(--tag-color-hover)`; return (
(); public isMultiSuggestDropdownActive = false; public isConfigModalOpen = false; + public conditionsRequiringValue = [ + "equals", + "contains", + "doesNotContain", + "startsWith", + "endsWith", + "is", + "isNot", + ">", + "<", + ">=", + "<=", + "before", + "onOrBefore", + "after", + "onOrAfter", + ]; constructor( hostEl: HTMLElement, plugin: TaskBoard, app: App, - private leafId?: string | undefined, - activeBoardIndex?: number, - private initialFilterState?: RootFilterState + currentBoardID: string, + private initialFilterState?: RootFilterState, ) { super(); this.hostEl = hostEl; this.plugin = plugin; this.app = app; - this.pluginSettings = plugin.settings; - this.activeBoardIndex = activeBoardIndex; + this.currentBoardID = currentBoardID; } onload() { // If initial filter state is provided (for column filters), use it if (this.initialFilterState) { this.rootFilterState = this.initialFilterState; + + // Clean up any invalid filter groups in the initial state + if ( + this.rootFilterState.filterGroups && + Array.isArray(this.rootFilterState.filterGroups) + ) { + this.rootFilterState.filterGroups = + this.rootFilterState.filterGroups.filter( + (groupData) => + groupData && + typeof groupData === "object" && + groupData.groupCondition && + Array.isArray(groupData.filters), + ); + } } else { /** * Otherwise, load from localStorage (for board filters) @@ -95,7 +123,7 @@ export class TaskFilterComponent extends Component { // } else { // if (savedState) { // // If it exists but failed validation - // console.warn( + // bugreporterInstance.addToLogs( // "Task Filter: Invalid data in local storage. Resetting to default state." // ); // } @@ -159,7 +187,7 @@ export class TaskFilterComponent extends Component { }); const rootConditionDropdown = new DropdownComponent( - rootConditionSection + rootConditionSection, ) .addOptions({ any: t("any"), @@ -207,7 +235,7 @@ export class TaskFilterComponent extends Component { }, (iconEl) => { setIcon(iconEl, "plus"); - } + }, ); el.createEl("span", { cls: "add-filter-group-btn-text", @@ -217,7 +245,7 @@ export class TaskFilterComponent extends Component { this.registerDomEvent(el, "click", () => { this.addFilterGroup(); }); - } + }, ); // Filter Configuration Buttons Section (only show if plugin is available) @@ -241,13 +269,13 @@ export class TaskFilterComponent extends Component { (iconEl) => { setIcon(iconEl, "save"); setTooltip(el, t("save-current-filter")); - } + }, ); this.registerDomEvent(el, "click", async () => { this.openSaveConfigModal(); }); - } + }, ); // Load Configuration Button @@ -265,18 +293,36 @@ export class TaskFilterComponent extends Component { (iconEl) => { setIcon(iconEl, "folder-open"); setTooltip(el, t("load-saved-filter")); - } + }, ); this.registerDomEvent(el, "click", async () => { this.openLoadConfigModal(); }); - } + }, ); } // Re-populate filter groups from state - this.rootFilterState.filterGroups.forEach((groupData) => { + // Filter out null/undefined/invalid filter groups to prevent corruption issues + const validFilterGroups = this.rootFilterState.filterGroups.filter( + (groupData) => + groupData && + typeof groupData === "object" && + groupData.groupCondition && + Array.isArray(groupData.filters), + ); + + // If invalid groups were found, update the state to remove them + if ( + validFilterGroups.length !== + this.rootFilterState.filterGroups.length + ) { + this.rootFilterState.filterGroups = validFilterGroups; + this.saveStateToLocalStorage(); + } + + validFilterGroups.forEach((groupData) => { const groupElement = this.createFilterGroupElement(groupData); this.filterGroupsContainerEl.appendChild(groupElement); }); @@ -312,9 +358,9 @@ export class TaskFilterComponent extends Component { }, (iconEl) => { setIcon(iconEl, "grip-vertical"); - } + }, ); - } + }, ); groupHeaderLeft.createEl("label", { @@ -334,13 +380,13 @@ export class TaskFilterComponent extends Component { this.saveStateToLocalStorage(); this.updateFilterConjunctions( newGroupEl.querySelector(".filters-list") as HTMLElement, - selectedValue + selectedValue, ); }) .setValue(groupData.groupCondition); groupConditionSelect.selectEl.toggleClass( ["group-condition-select", "compact-select"], - true + true, ); groupHeaderLeft.createEl("span", { @@ -378,7 +424,7 @@ export class TaskFilterComponent extends Component { .setTooltip(t("remove-filter-group")) .onClick(() => { const filtersListElForSortable = newGroupEl.querySelector( - ".filters-list" + ".filters-list", ) as HTMLElement; if ( filtersListElForSortable && @@ -392,7 +438,7 @@ export class TaskFilterComponent extends Component { this.rootFilterState.filterGroups = this.rootFilterState.filterGroups.filter( - (g) => g.id !== groupData.id + (g) => g.id !== groupData.id, ); this.saveStateToLocalStorage(); newGroupEl.remove(); @@ -400,7 +446,7 @@ export class TaskFilterComponent extends Component { if ( nextSibling && nextSibling.classList.contains( - "filter-group-separator-container" + "filter-group-separator-container", ) ) { nextSibling.remove(); @@ -409,7 +455,7 @@ export class TaskFilterComponent extends Component { if ( prevSibling && prevSibling.classList.contains( - "filter-group-separator-container" + "filter-group-separator-container", ) ) { prevSibling.remove(); @@ -429,7 +475,7 @@ export class TaskFilterComponent extends Component { groupData.filters.forEach((filterData) => { const filterElement = this.createFilterItemElement( filterData, - groupData + groupData, ); filtersListEl.appendChild(filterElement); }); @@ -452,7 +498,7 @@ export class TaskFilterComponent extends Component { }, (iconEl) => { setIcon(iconEl, "plus"); - } + }, ); el.createEl("span", { cls: "add-filter-btn-text", @@ -462,7 +508,7 @@ export class TaskFilterComponent extends Component { this.registerDomEvent(el, "click", () => { this.addFilterToGroup(groupData, filtersListEl); }); - } + }, ); return newGroupEl; @@ -470,12 +516,14 @@ export class TaskFilterComponent extends Component { private addFilterGroup( groupDataToClone: FilterGroup | null = null, - insertAfterElement: HTMLElement | null = null + insertAfterElement: HTMLElement | null = null, ): void { // Ensure the container is initialized if (!this.filterGroupsContainerEl) { - console.warn( - "TaskFilterComponent: filterGroupsContainerEl not initialized yet" + bugReporterManagerInsatance.addToLogs( + 168, + "filterGroupsContainerEl not initialized yet", + "ViewTaskFilter.ts/addFilterGroup", ); return; } @@ -504,8 +552,8 @@ export class TaskFilterComponent extends Component { const groupIndex = insertAfterElement ? this.rootFilterState.filterGroups.findIndex( - (g) => g.id === insertAfterElement.id - ) + 1 + (g) => g.id === insertAfterElement.id, + ) + 1 : this.rootFilterState.filterGroups.length; this.rootFilterState.filterGroups.splice(groupIndex, 0, newGroupData); @@ -518,7 +566,7 @@ export class TaskFilterComponent extends Component { ) { this.filterGroupsContainerEl.insertBefore( newGroupElement, - insertAfterElement.nextSibling + insertAfterElement.nextSibling, ); } else { this.filterGroupsContainerEl.appendChild(newGroupElement); @@ -530,7 +578,7 @@ export class TaskFilterComponent extends Component { ) { this.addFilterToGroup( newGroupData, - newGroupElement.querySelector(".filters-list") as HTMLElement + newGroupElement.querySelector(".filters-list") as HTMLElement, ); } else if ( groupDataToClone && @@ -539,7 +587,7 @@ export class TaskFilterComponent extends Component { ) { this.addFilterToGroup( newGroupData, - newGroupElement.querySelector(".filters-list") as HTMLElement + newGroupElement.querySelector(".filters-list") as HTMLElement, ); } @@ -550,7 +598,7 @@ export class TaskFilterComponent extends Component { // --- Filter Item Management --- private createFilterItemElement( filterData: Filter, - groupData: FilterGroup + groupData: FilterGroup, ): HTMLElement { const newFilterEl = this.hostEl.createEl("div", { attr: { id: filterData.id }, @@ -613,29 +661,16 @@ export class TaskFilterComponent extends Component { conditionSelect, valueInput, valueSelect, - dropdownInputContainer + dropdownInputContainer, ); }); const toggleValueInputVisibility = ( currentCond: string, - propertyType: string + propertyType: string, ) => { - const conditionsRequiringValue = [ - "equals", - "contains", - "doesNotContain", - "startsWith", - "endsWith", - "is", - "isNot", - ">", - "<", - ">=", - "<=", - ]; let valueActuallyNeeded = - conditionsRequiringValue.includes(currentCond); + this.conditionsRequiringValue.includes(currentCond); if ( propertyType === "completed" && @@ -701,13 +736,13 @@ export class TaskFilterComponent extends Component { .setTooltip(t("remove-filter")) .onClick(() => { groupData.filters = groupData.filters.filter( - (f) => f.id !== filterData.id + (f) => f.id !== filterData.id, ); this.saveStateToLocalStorage(); newFilterEl.remove(); this.updateFilterConjunctions( newFilterEl.parentElement as HTMLElement, - groupData.groupCondition + groupData.groupCondition, ); }); removeFilterBtn.extraSettingsEl.addClasses([ @@ -722,7 +757,7 @@ export class TaskFilterComponent extends Component { conditionSelect, valueInput, valueSelect, - dropdownInputContainer + dropdownInputContainer, ); return newFilterEl; @@ -730,7 +765,7 @@ export class TaskFilterComponent extends Component { private addFilterToGroup( groupData: FilterGroup, - filtersListEl: HTMLElement + filtersListEl: HTMLElement, ): void { const newFilterId = generateIdForFilters(); const newFilterData: Filter = { @@ -744,7 +779,7 @@ export class TaskFilterComponent extends Component { const newFilterElement = this.createFilterItemElement( newFilterData, - groupData + groupData, ); filtersListEl.appendChild(newFilterElement); @@ -758,7 +793,7 @@ export class TaskFilterComponent extends Component { conditionSelect: DropdownComponent, valueInput: HTMLInputElement, valueSelect: DropdownComponent, - dropdownInputContainer: HTMLElement + dropdownInputContainer: HTMLElement, ): void { const property = filterData.property; @@ -788,12 +823,11 @@ export class TaskFilterComponent extends Component { // let dropdownInput: DropdownComponent | null = null; // filterItemEl.removeChild(dropdownInputContainer); - if (valueSelect) { - console.log("Removing dropdown input"); - // dropdownInput.disabled = true; - // dropdownInput.type = "text"; - // dropdownInput = null; - } + // if (valueSelect) { + // dropdownInput.disabled = true; + // dropdownInput.type = "text"; + // dropdownInput = null; + // } switch (property) { case "content": @@ -850,30 +884,56 @@ export class TaskFilterComponent extends Component { break; case "status": // valueInput.type = "text"; - valueInput.style.display = "none"; // // First remove the older dropdown options present inside valueSelect // if(valueSelect.selectEl.options.length > 0) { // valueSelect. // } - valueSelect.addOptions( - getCustomStatusOptionsForDropdown( - this.plugin.settings.data.globalSettings - .customStatuses - ).reduce( - ( - acc: Record, - opt: statusDropDownOption - ) => { - acc[opt.value] = opt.text; - return acc; - }, - {} - ) + + const statusOptions = getCustomStatusOptionsForDropdown( + this.plugin.settings.data.customStatuses, + { mode: "grouped" }, ); + const optionsRecord: Record = {}; + if (statusOptions.type === "grouped") { + statusOptions.groups.forEach((group) => { + // Add visual group separator (disabled option) + optionsRecord[`__group_${group.type}__`] = + `── ${group.label} ──`; + + // Add actual status options under the group + group.options.forEach((opt) => { + optionsRecord[opt.value] = opt.label; + }); + }); + } else { + // Fallback for flat output + statusOptions.options.forEach((opt) => { + optionsRecord[opt.value] = opt.label; + }); + } + valueSelect.addOptions(optionsRecord); + // Disable and style group header options after DOM render + setTimeout(() => { + Array.from(valueSelect.selectEl.options).forEach( + (option) => { + if (option.value.startsWith("__group_")) { + option.disabled = true; + option.style.cssText = ` + color: var(--text-muted); + font-weight: var(--font-semibold); + background: var(--background-secondary); + pointer-events: none; + user-select: none; + `; + } + }, + ); + }, 0); + valueSelect.setValue( filterData.value || - getPriorityOptionsForDropdown()[0].value.toString() + getPriorityOptionsForDropdown()[0].value.toString(), ); valueSelect.onChange((newValue) => { filterData.value = newValue; @@ -936,17 +996,17 @@ export class TaskFilterComponent extends Component { getPriorityOptionsForDropdown().reduce( ( acc: Record, - opt: priorityDropDownOption + opt: priorityDropDownOption, ) => { acc[opt.value] = opt.text; return acc; }, - {} - ) + {}, + ), ); valueSelect.setValue( filterData.value || - getPriorityOptionsForDropdown()[0].value.toString() + getPriorityOptionsForDropdown()[0].value.toString(), ); valueSelect.onChange((newValue) => { filterData.value = Number(newValue); @@ -975,6 +1035,14 @@ export class TaskFilterComponent extends Component { value: "isNot", text: t("is-not"), }, + { + value: ">", + text: ">", + }, + { + value: "<", + text: "<", + }, { value: ">=", text: ">=", @@ -1041,20 +1109,20 @@ export class TaskFilterComponent extends Component { text: t("is-not"), }, { - value: ">", - text: ">", + value: "before", + text: t("before"), }, { - value: "<", - text: "<", + value: "onOrBefore", + text: t("on-or-before"), }, { - value: ">=", - text: ">=", + value: "after", + text: t("after"), }, { - value: "<=", - text: "<=", + value: "onOrAfter", + text: t("on-or-after"), }, { value: "isEmpty", @@ -1075,20 +1143,20 @@ export class TaskFilterComponent extends Component { text: t("is-not"), }, { - value: ">", - text: ">", + value: "before", + text: t("before"), }, { - value: "<", - text: "<", + value: "onOrBefore", + text: t("on-or-before"), }, { - value: ">=", - text: ">=", + value: "after", + text: t("after"), }, { - value: "<=", - text: "<=", + value: "onOrAfter", + text: t("on-or-after"), }, { value: "isEmpty", @@ -1153,39 +1221,33 @@ export class TaskFilterComponent extends Component { conditionSelect.selectEl.empty(); conditionOptions.forEach((opt) => - conditionSelect.addOption(opt.value, opt.text) + conditionSelect.addOption(opt.value, opt.text), ); const currentSelectedCondition = filterData.condition; - let conditionChanged = false; + // let conditionChanged = false; if ( conditionOptions.some( - (opt) => opt.value === currentSelectedCondition + (opt) => opt.value === currentSelectedCondition, ) ) { conditionSelect.setValue(currentSelectedCondition); + // conditionChanged = true; } else if (conditionOptions.length > 0) { conditionSelect.setValue(conditionOptions[0].value); filterData.condition = conditionOptions[0].value; - conditionChanged = true; + // conditionChanged = true; } const finalConditionVal = conditionSelect.getValue(); - const conditionsRequiringValue = [ - "equals", - "contains", - "doesNotContain", - "startsWith", - "endsWith", - "is", - "isNot", - ">", - "<", - ">=", - "<=", - ]; let valueActuallyNeeded = - conditionsRequiringValue.includes(finalConditionVal); + this.conditionsRequiringValue.includes(finalConditionVal); + console.log( + "finalConditionVal : ", + finalConditionVal, + "\nvalueActuallyNeeded : ", + valueActuallyNeeded, + ); // if ( // property === "completed" && // (finalConditionVal === "isTrue" || finalConditionVal === "isFalse") @@ -1241,9 +1303,9 @@ export class TaskFilterComponent extends Component { } } - if (conditionChanged || valueChanged) { - this.saveStateToLocalStorage(); - } + // if (conditionChanged || valueChanged) { + // this.saveStateToLocalStorage(); + // } if (valueInput instanceof HTMLInputElement) { // Setup MultiSuggest for appropriate properties @@ -1254,10 +1316,13 @@ export class TaskFilterComponent extends Component { private setupMultiSuggest( property: string, valueInput: HTMLInputElement, - filterData: Filter + filterData: Filter, ): void { // Only setup suggestions for specific properties const propertiesWithSuggestions = ["tags", "filePath"]; + if (!propertiesWithSuggestions.includes(property)) { + return; + } // Clean up existing MultiSuggest instance if it exists const existingInstance = this.multiSuggestInstances.get(valueInput); @@ -1266,16 +1331,12 @@ export class TaskFilterComponent extends Component { this.multiSuggestInstances.delete(valueInput); } - if (!propertiesWithSuggestions.includes(property)) { - return; - } - let suggestions: string[] = []; switch (property) { // case "status": // suggestions = getStatusSuggestions( - // this.pluginSettings.data.globalSettings + // this.pluginSettings.data // .customStatuses // ); // break; @@ -1298,7 +1359,7 @@ export class TaskFilterComponent extends Component { valueInput, new Set(suggestions), onSelectCallback, - this.app + this.app, ); // Store instance in WeakMap for cleanup @@ -1308,13 +1369,13 @@ export class TaskFilterComponent extends Component { // --- UI Updates (Conjunctions, Separators) --- private updateFilterConjunctions( filtersListEl: HTMLElement | null, - groupCondition: "all" | "any" | "none" = "all" + groupCondition: "all" | "any" | "none" = "all", ): void { if (!filtersListEl) return; const filters = filtersListEl.querySelectorAll(".filter-item"); filters.forEach((filter, index) => { const conjunctionElement = filter.querySelector( - ".filter-conjunction" + ".filter-conjunction", ) as HTMLElement; if (conjunctionElement) { if (index !== 0) { @@ -1346,7 +1407,7 @@ export class TaskFilterComponent extends Component { .forEach((sep) => sep.remove()); const groups = Array.from( - this.filterGroupsContainerEl?.children || [] + this.filterGroupsContainerEl?.children || [], ).filter((child) => child.classList.contains("filter-group")); if (groups.length > 1) { @@ -1367,7 +1428,7 @@ export class TaskFilterComponent extends Component { separator.textContent = separatorText.toUpperCase(); group.parentNode?.insertBefore( separatorContainer, - group.nextSibling + group.nextSibling, ); } }); @@ -1398,12 +1459,12 @@ export class TaskFilterComponent extends Component { const movedGroup = this.rootFilterState.filterGroups.splice( sortableEvent.oldDraggableIndex, - 1 + 1, )[0]; this.rootFilterState.filterGroups.splice( sortableEvent.newDraggableIndex, 0, - movedGroup + movedGroup, ); this.saveStateToLocalStorage(); this.updateGroupSeparators(); @@ -1414,7 +1475,7 @@ export class TaskFilterComponent extends Component { // --- Filter State Management --- private updateFilterState( filterGroups: FilterGroup[], - rootCondition: "all" | "any" | "none" + rootCondition: "all" | "any" | "none", ): void { this.rootFilterState.filterGroups = filterGroups; this.rootFilterState.rootCondition = rootCondition; @@ -1442,7 +1503,11 @@ export class TaskFilterComponent extends Component { this.groupsSortable = undefined; } } catch (error) { - console.warn("Error destroying groups sortable:", error); + bugReporterManagerInsatance.addToLogs( + 169, + `Error destroying groups sortable: ${String(error)}`, + "ViewTaskFilter.ts/loadFilterState", + ); this.groupsSortable = undefined; } @@ -1458,29 +1523,45 @@ export class TaskFilterComponent extends Component { (listEl as any).sortableInstance = undefined; } } catch (error) { - console.warn( - "Error destroying filter list sortable:", - error + bugReporterManagerInsatance.addToLogs( + 170, + `Error destroying filter list sortable: ${String(error)}`, + "ViewTaskFilter.ts/loadFilterState", ); (listEl as any).sortableInstance = undefined; } }); this.rootFilterState = JSON.parse(JSON.stringify(state)); + + // Clean up any invalid filter groups that might exist in loaded state + if ( + this.rootFilterState.filterGroups && + Array.isArray(this.rootFilterState.filterGroups) + ) { + this.rootFilterState.filterGroups = + this.rootFilterState.filterGroups.filter( + (groupData) => + groupData && + typeof groupData === "object" && + groupData.groupCondition && + Array.isArray(groupData.filters), + ); + } + this.saveStateToLocalStorage(); this.render(); } - // --- Local Storage Management --- + /** + * This feature is in disabled state, hence no need to store anything in localStorage. + * + * @todo See this if required sometime in future. + */ private saveStateToLocalStorage( - triggerRealtimeUpdate: boolean = true + triggerRealtimeUpdate: boolean = true, ): void { - /** - * This feature is in disabled state, hence no need to store anything in localStorage. - * - * @todo See this if required sometime in future. - */ // if (this.app) { // this.app.saveLocalStorage( // this.leafId @@ -1500,20 +1581,20 @@ export class TaskFilterComponent extends Component { // --- Filter Configuration Management --- private openSaveConfigModal(): void { - if (!this.plugin || this.activeBoardIndex === undefined) return; + if (!this.plugin) return; - const modal = new FilterConfigModal( + const modal = new BoardFiltersStoreModal( this.app, this.plugin, "save", - this.activeBoardIndex, + this.currentBoardID, this.getFilterState(), (config: SavedFilterConfig) => { // Optional: Handle successful save new Notice( - `${t("filter-configs-saved-successfully")} : ${config.name}` + `${t("filter-configs-saved-successfully")} : ${config.name}`, ); - } + }, ); modal.setCloseCallback(() => { this.isConfigModalOpen = false; @@ -1523,13 +1604,13 @@ export class TaskFilterComponent extends Component { } private openLoadConfigModal(): void { - if (!this.plugin || this.activeBoardIndex === undefined) return; + if (!this.plugin) return; - const modal = new FilterConfigModal( + const modal = new BoardFiltersStoreModal( this.app, this.plugin, "load", - this.activeBoardIndex, + this.currentBoardID, undefined, undefined, (config: SavedFilterConfig) => { @@ -1538,9 +1619,9 @@ export class TaskFilterComponent extends Component { new Notice( `${t("filter-configuration-loaded-successfully")} : ${ config.name - }` + }`, ); - } + }, ); modal.setCloseCallback(() => { this.isConfigModalOpen = false; @@ -1551,5 +1632,5 @@ export class TaskFilterComponent extends Component { } export function generateIdForFilters(): string { - return `id-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + return `id-${Date.now()}-${generateRandomTempTaskId()}`; } diff --git a/src/components/BoardFilters/FilterConfigModal.ts b/src/components/AdvancedFilterer/LoadSavedFiltersModal.ts similarity index 77% rename from src/components/BoardFilters/FilterConfigModal.ts rename to src/components/AdvancedFilterer/LoadSavedFiltersModal.ts index add49268..3c824430 100644 --- a/src/components/BoardFilters/FilterConfigModal.ts +++ b/src/components/AdvancedFilterer/LoadSavedFiltersModal.ts @@ -1,33 +1,31 @@ +import { t } from "i18next"; import { App, Modal, Setting, Notice, DropdownComponent } from "obsidian"; -import type TaskBoard from "main"; -import { t } from "src/utils/lang/helper"; -import { - FilterGroup, - RootFilterState, - SavedFilterConfig, -} from "src/interfaces/BoardConfigs"; - -export class FilterConfigModal extends Modal { +import type { RootFilterState, SavedFilterConfig, FilterGroup, Board } from "../../interfaces/BoardConfigs.js"; +import { bugReporterManagerInsatance } from "../../managers/BugReporter.js"; +import { generateRandomTempTaskId } from "../../utils/TaskItemUtils.js"; +import type TaskBoard from "../../../main.js"; + +export class BoardFiltersStoreModal extends Modal { private plugin: TaskBoard; private mode: "save" | "load"; + private currentBoardID: string; private currentFilterState?: RootFilterState; private onSave?: (config: SavedFilterConfig) => void; private onLoad?: (config: SavedFilterConfig) => void; - private activeBoardIndex: number; constructor( app: App, plugin: TaskBoard, mode: "save" | "load", - activeBoardIndex: number, + currentBoardID: string, currentFilterState?: RootFilterState, onSave?: (config: SavedFilterConfig) => void, - onLoad?: (config: SavedFilterConfig) => void + onLoad?: (config: SavedFilterConfig) => void, ) { super(app); this.plugin = plugin; this.mode = mode; - this.activeBoardIndex = activeBoardIndex; + this.currentBoardID = currentBoardID; this.currentFilterState = currentFilterState; this.onSave = onSave; this.onLoad = onLoad; @@ -90,20 +88,20 @@ export class FilterConfigModal extends Modal { }); } - private renderLoadMode() { + private async renderLoadMode() { const { contentEl } = this; contentEl.createEl("h2", { text: t("load-filter-configuration") }); - const board = - this.plugin.settings.data.boardConfigs[this.activeBoardIndex]; - if (!board.filterConfig) { + const board: Board | null = + await this.plugin.taskBoardFileManager.loadBoardUsingID(this.currentBoardID); + if (board && !board?.filterConfig) { board.filterConfig = { enableSavedFilters: true, savedConfigs: [], }; } - const savedConfigs = board.filterConfig.savedConfigs; + const savedConfigs = board!.filterConfig!.savedConfigs; if (savedConfigs.length === 0) { contentEl.createEl("p", { @@ -124,7 +122,7 @@ export class FilterConfigModal extends Modal { .addDropdown((dropdown: DropdownComponent) => { dropdown.addOption( "", - t("select-a-saved-filter-configuration") + t("select-a-saved-filter-configuration"), ); savedConfigs.forEach((config) => { @@ -172,7 +170,7 @@ export class FilterConfigModal extends Modal { (this as any).detailsContainer = detailsContainer; } - private updateConfigDetails(configId: string) { + private async updateConfigDetails(configId: string) { const detailsContainer = (this as any).detailsContainer; if (!detailsContainer) return; @@ -181,11 +179,11 @@ export class FilterConfigModal extends Modal { if (!configId) return; const board = - this.plugin.settings.data.boardConfigs[this.activeBoardIndex]; - if (!board.filterConfig) return; + await this.plugin.taskBoardFileManager.loadBoardUsingID(this.currentBoardID); + if (board && !board.filterConfig) return; - const config = board.filterConfig.savedConfigs.find( - (c: SavedFilterConfig) => c.id === configId + const config = board!.filterConfig!.savedConfigs.find( + (c: SavedFilterConfig) => c.id === configId, ); if (!config) return; @@ -198,14 +196,14 @@ export class FilterConfigModal extends Modal { detailsContainer.createEl("p", { text: `${t("created")}: ${new Date( - config.createdAt + config.createdAt, ).toLocaleString()}`, cls: "filter-config-meta", }); detailsContainer.createEl("p", { text: `${t("updated")}: ${new Date( - config.updatedAt + config.updatedAt, ).toLocaleString()}`, cls: "filter-config-meta", }); @@ -219,7 +217,7 @@ export class FilterConfigModal extends Modal { const groupCount = config.filterState.filterGroups.length; const totalFilters = config.filterState.filterGroups.reduce( (sum: number, group: FilterGroup) => sum + group.filters.length, - 0 + 0, ); filterSummary.createEl("p", { @@ -246,9 +244,7 @@ export class FilterConfigModal extends Modal { const now = new Date().toISOString(); const config: SavedFilterConfig = { - id: `filter-config-${Date.now()}-${Math.random() - .toString(36) - .substr(2, 9)}`, + id: `filter-config-${Date.now()}-${generateRandomTempTaskId()}`, name: name.trim(), description: description.trim() || undefined, filterState: JSON.parse(JSON.stringify(this.currentFilterState)), @@ -258,14 +254,14 @@ export class FilterConfigModal extends Modal { try { const board = - this.plugin.settings.data.boardConfigs[this.activeBoardIndex]; - if (!board.filterConfig) { + await this.plugin.taskBoardFileManager.loadBoardUsingID(this.currentBoardID); + if (board && !board.filterConfig) { board.filterConfig = { enableSavedFilters: true, savedConfigs: [], }; } - board.filterConfig.savedConfigs.push(config); + board!.filterConfig!.savedConfigs.push(config); await this.plugin.saveSettings(); if (this.onSave) { @@ -274,7 +270,11 @@ export class FilterConfigModal extends Modal { this.close(); } catch (error) { - console.error("Failed to save filter configuration:", error); + bugReporterManagerInsatance.addToLogs( + 111, + String(error), + "BoardFiltersStoreModal.ts/saveConfiguration", + ); new Notice(t("failed-to-save-filter-configuration")); } } @@ -286,11 +286,11 @@ export class FilterConfigModal extends Modal { } const board = - this.plugin.settings.data.boardConfigs[this.activeBoardIndex]; - if (!board.filterConfig) return; + await this.plugin.taskBoardFileManager.loadBoardUsingID(this.currentBoardID); + if (!board || !board.filterConfig) return; const config = board.filterConfig.savedConfigs.find( - (c: SavedFilterConfig) => c.id === configId + (c: SavedFilterConfig) => c.id === configId, ); if (!config) { @@ -305,7 +305,11 @@ export class FilterConfigModal extends Modal { this.close(); } catch (error) { - console.error("Failed to load filter configuration:", error); + bugReporterManagerInsatance.addToLogs( + 112, + String(error), + "BoardFiltersStoreModal.ts/loadConfiguration", + ); new Notice(t("failed-to-load-filter-configuration")); } } @@ -316,12 +320,11 @@ export class FilterConfigModal extends Modal { return; } - const board = - this.plugin.settings.data.boardConfigs[this.activeBoardIndex]; - if (!board.filterConfig) return; + const board = await this.plugin.taskBoardFileManager.loadBoardUsingID(this.currentBoardID); + if (!board || !board.filterConfig) return; const config = board.filterConfig.savedConfigs.find( - (c: SavedFilterConfig) => c.id === configId + (c: SavedFilterConfig) => c.id === configId, ); if (!config) { @@ -366,12 +369,12 @@ export class FilterConfigModal extends Modal { try { const board = - this.plugin.settings.data.boardConfigs[this.activeBoardIndex]; - if (!board.filterConfig) return; + await this.plugin.taskBoardFileManager.loadBoardUsingID(this.currentBoardID); + if (!board || !board.filterConfig) return; board.filterConfig.savedConfigs = board.filterConfig.savedConfigs.filter( - (c: SavedFilterConfig) => c.id !== configId + (c: SavedFilterConfig) => c.id !== configId, ); await this.plugin.saveSettings(); @@ -382,18 +385,22 @@ export class FilterConfigModal extends Modal { this.close(); // Reopen in load mode to refresh the list - const newModal = new FilterConfigModal( + const newModal = new BoardFiltersStoreModal( this.app, this.plugin, "load", - this.activeBoardIndex, + this.currentBoardID, undefined, this.onSave, - this.onLoad + this.onLoad, ); newModal.open(); } catch (error) { - console.error("Failed to delete filter configuration:", error); + bugReporterManagerInsatance.addToLogs( + 113, + String(error), + "BoardFiltersStoreModal.ts/deleteConfiguration", + ); new Notice(t("failed-to-delete-filter-configuration")); } } diff --git a/src/components/BoardFilters/ViewTaskFilterModal.ts b/src/components/AdvancedFilterer/Modal.ts similarity index 55% rename from src/components/BoardFilters/ViewTaskFilterModal.ts rename to src/components/AdvancedFilterer/Modal.ts index 9b3358d7..3284cfde 100644 --- a/src/components/BoardFilters/ViewTaskFilterModal.ts +++ b/src/components/AdvancedFilterer/Modal.ts @@ -1,13 +1,14 @@ +import { t } from "i18next"; import { Modal } from "obsidian"; -import type TaskBoard from "main"; -import { t } from "src/utils/lang/helper"; -import { RootFilterState } from "src/interfaces/BoardConfigs"; -import { TaskFilterComponent } from "./ViewTaskFilter"; +import TaskBoard from "../../../main.js"; +import { RootFilterState } from "../../interfaces/BoardConfigs.js"; +import { bugReporterManagerInsatance } from "../../managers/BugReporter.js"; +import { AdvancedFilterComponent } from "./Component.js"; -export class ViewTaskFilterModal extends Modal { +export class AdvancedFilterModal extends Modal { private plugin: TaskBoard; - public activeBoardIndex?: number; - public taskFilterComponent: TaskFilterComponent | null; + private currentBoardID: string; + public taskFilterComponent: AdvancedFilterComponent | null; private columnOrBoardName?: string; private initialFilterState?: RootFilterState; public filterCloseCallback: @@ -17,14 +18,13 @@ export class ViewTaskFilterModal extends Modal { constructor( plugin: TaskBoard, forColumn: boolean, - private leafId?: string, - activeBoardIndex?: number, + currentBoardID: string, columnOrBoardName?: string, - initialFilterState?: RootFilterState + initialFilterState?: RootFilterState, ) { super(plugin.app); this.plugin = plugin; - this.activeBoardIndex = activeBoardIndex; + this.currentBoardID = currentBoardID; this.columnOrBoardName = columnOrBoardName; this.initialFilterState = initialFilterState; @@ -32,11 +32,11 @@ export class ViewTaskFilterModal extends Modal { if (forColumn) { this.setTitle( - t("column-filters-for") + " " + this.columnOrBoardName + t("column-filters-for") + " " + this.columnOrBoardName, ); } else { this.setTitle( - t("board-filters-for") + " " + this.columnOrBoardName + t("board-filters-for") + " " + this.columnOrBoardName, ); } } @@ -45,13 +45,12 @@ export class ViewTaskFilterModal extends Modal { const { contentEl } = this; contentEl.empty(); - this.taskFilterComponent = new TaskFilterComponent( + this.taskFilterComponent = new AdvancedFilterComponent( this.contentEl, this.plugin, this.app, - this.leafId, - this.activeBoardIndex, - this.initialFilterState + this.currentBoardID, + this.initialFilterState, ); // Ensure the component is properly loaded this.taskFilterComponent.onload(); @@ -66,9 +65,10 @@ export class ViewTaskFilterModal extends Modal { filterState = this.taskFilterComponent.getFilterState(); this.taskFilterComponent.onunload(); } catch (error) { - console.error( - "Failed to get filter state before modal close", - error + bugReporterManagerInsatance.addToLogs( + 114, + String(error), + "AdvancedFilterModal.ts/onClose", ); } } @@ -79,7 +79,11 @@ export class ViewTaskFilterModal extends Modal { try { this.filterCloseCallback(filterState); } catch (error) { - console.error("Error in filter close callback", error); + bugReporterManagerInsatance.addToLogs( + 115, + String(error), + "AdvancedFilterModal.ts/onClose", + ); } } } diff --git a/src/components/BoardFilters/ViewTaskFilterPopover.ts b/src/components/AdvancedFilterer/Popover.ts similarity index 85% rename from src/components/BoardFilters/ViewTaskFilterPopover.ts rename to src/components/AdvancedFilterer/Popover.ts index 17edc0c7..f5ef4e6a 100644 --- a/src/components/BoardFilters/ViewTaskFilterPopover.ts +++ b/src/components/AdvancedFilterer/Popover.ts @@ -1,43 +1,40 @@ -// /src/components/BoardFilters/ViewTaskFilterPopover.ts +// /src/components/BoardFilters/AdvancedFilterPopover.ts import { App } from "obsidian"; import { CloseableComponent, Component } from "obsidian"; import { createPopper, Instance as PopperInstance } from "@popperjs/core"; -import type TaskBoard from "main"; -import { t } from "src/utils/lang/helper"; -import { RootFilterState } from "src/interfaces/BoardConfigs"; -import { TaskFilterComponent } from "./ViewTaskFilter"; - -export class ViewTaskFilterPopover - extends Component - implements CloseableComponent -{ +import { t } from "i18next"; +import TaskBoard from "../../../main.js"; +import { RootFilterState } from "../../interfaces/BoardConfigs.js"; +import { bugReporterManagerInsatance } from "../../managers/BugReporter.js"; +import { AdvancedFilterComponent } from "./Component.js"; + +export class AdvancedFilterPopover extends Component implements CloseableComponent { private plugin: TaskBoard; private app: App; - public popoverRef: HTMLDivElement | null = null; public forColumn: boolean; - public taskFilterComponent!: TaskFilterComponent; + public currentBoardID: string; + public popoverRef: HTMLDivElement | null = null; + public taskFilterComponent!: AdvancedFilterComponent; private win: Window; private scrollParent: HTMLElement | Window; private popperInstance: PopperInstance | null = null; public onClose: ((filterState?: RootFilterState) => void) | null = null; - private activeBoardIndex?: number; private columnOrBoardName?: string; private initialFilterState?: RootFilterState; constructor( plugin: TaskBoard, forColumn: boolean, - private leafId?: string | undefined, - activeBoardIndex?: number, + currentBoardID: string, columnOrBoardName?: string, - initialFilterState?: RootFilterState + initialFilterState?: RootFilterState, ) { super(); this.plugin = plugin; this.app = plugin.app; this.forColumn = forColumn; - this.activeBoardIndex = activeBoardIndex; + this.currentBoardID = currentBoardID; this.columnOrBoardName = columnOrBoardName; this.initialFilterState = initialFilterState; this.win = plugin.app.workspace.containerEl.win || window; @@ -84,13 +81,12 @@ export class ViewTaskFilterPopover }); // Create metadata editor, use compact mode - this.taskFilterComponent = new TaskFilterComponent( + this.taskFilterComponent = new AdvancedFilterComponent( taskFilterContainer, this.plugin, this.app, - this.leafId, - this.activeBoardIndex, - this.initialFilterState + this.currentBoardID, + this.initialFilterState, ); // Ensure the component is properly loaded this.taskFilterComponent.onload(); @@ -151,7 +147,7 @@ export class ViewTaskFilterPopover }, }, ], - } + }, ); } @@ -221,7 +217,11 @@ export class ViewTaskFilterPopover try { filterState = this.taskFilterComponent.getFilterState(); } catch (error) { - console.error("Failed to get filter state before close", error); + bugReporterManagerInsatance.addToLogs( + 116, + String(error), + "AdvancedFilterPopover.ts/close", + ); } } @@ -245,7 +245,11 @@ export class ViewTaskFilterPopover try { this.onClose(filterState); } catch (error) { - console.error("Error in onClose callback", error); + bugReporterManagerInsatance.addToLogs( + 117, + String(error), + "AdvancedFilterPopover.ts/close", + ); } } } diff --git a/src/components/BoardFilters/index.ts b/src/components/AdvancedFilterer/index.ts similarity index 69% rename from src/components/BoardFilters/index.ts rename to src/components/AdvancedFilterer/index.ts index 7046694b..33c372dc 100644 --- a/src/components/BoardFilters/index.ts +++ b/src/components/AdvancedFilterer/index.ts @@ -1,7 +1,8 @@ /** * This BoardFilters component has been inspired from Bases filter and Task Genius plugin filters. All credits for this component go to the developer of Task Genius plugin. + * * Changes made to the original code: - * - Added type safetly at various places. + * - Added type safety at various places. * - This component can be used for both board as well as column. * - A heading for the popover and modal to display the column or board name. * - Input suggestion for various properties such as tags, priority, status, filePath, etc. @@ -9,8 +10,8 @@ * @url https://github.com/Quorafind/Obsidian-Task-Genius/blob/6307b018cae3c1a20e753127faac88492aac9ffc/src/components/features/task/filter/index.ts */ -import { TaskFilterComponent } from "./ViewTaskFilter"; -import { ViewTaskFilterModal } from "./ViewTaskFilterModal"; -import { ViewTaskFilterPopover } from "./ViewTaskFilterPopover"; +import { AdvancedFilterComponent } from "./Component.js"; +import { AdvancedFilterModal } from "./Modal.js"; +import { AdvancedFilterPopover } from "./Popover.js"; -export { TaskFilterComponent, ViewTaskFilterModal, ViewTaskFilterPopover }; +export { AdvancedFilterComponent, AdvancedFilterModal, AdvancedFilterPopover }; diff --git a/src/components/KanbanView/Column.tsx b/src/components/KanbanView/Column.tsx deleted file mode 100644 index dd79a24d..00000000 --- a/src/components/KanbanView/Column.tsx +++ /dev/null @@ -1,809 +0,0 @@ -// /src/components/Column.tsx - -/** - * Column component - DEPRECATED - * This component has been now deprecated, since the LazyColum.tsx component is performing its intended functionality of lazy-loading without any issue in all the tested environments. - * Will still keep this file in the project and update it with the changes from LazyColumn.tsx, just to have a fallback backup ready. And for other testing. - * @kind component - * @deprecated - */ - -import React, { memo, useCallback, useEffect, useMemo, useState, useRef } from 'react'; - -import { CSSProperties } from 'react'; -import TaskItem, { swimlaneDataProp } from './TaskItem'; -import { t } from 'src/utils/lang/helper'; -import TaskBoard from 'main'; -import { Board, ColumnData, RootFilterState } from 'src/interfaces/BoardConfigs'; -import { taskItem } from 'src/interfaces/TaskItem'; -import { Menu, Platform } from 'obsidian'; -import { ViewTaskFilterPopover } from 'src/components/BoardFilters/ViewTaskFilterPopover'; -import { eventEmitter } from 'src/services/EventEmitter'; -import { ViewTaskFilterModal } from 'src/components/BoardFilters'; -import { ConfigureColumnSortingModal } from 'src/modals/ConfigureColumnSortingModal'; -import { matchTagsWithWildcards } from 'src/utils/algorithms/ScanningFilterer'; -import { isRootFilterStateEmpty } from 'src/utils/algorithms/BoardFilterer'; -import { dragDropTasksManagerInsatance } from 'src/managers/DragDropTasksManager'; -import { bugReporterManagerInsatance } from 'src/managers/BugReporter'; - -type CustomCSSProperties = CSSProperties & { - '--task-board-column-width': string; -}; - -export interface ColumnProps { - plugin: TaskBoard; - columnIndex: number; - activeBoardData: Board; - collapsed?: boolean; - columnData: ColumnData; - tasksForThisColumn: taskItem[]; - swimlaneData?: swimlaneDataProp; - hideColumnHeader?: boolean; - headerOnly?: boolean; -} - - -const Column: React.FC = ({ - plugin, - columnIndex, - activeBoardData, - columnData, - tasksForThisColumn, - swimlaneData, - hideColumnHeader = false, - headerOnly = false, -}) => { - if (activeBoardData?.hideEmptyColumns && (tasksForThisColumn === undefined || tasksForThisColumn.length === 0)) { - return null; // Don't render the column if it has no tasks and empty columns are hidden - } - const [isDragOver, setIsDragOver] = useState(false); - const [insertIndex, setInsertIndex] = useState(null); - const insertIndexRef = useRef(null); - const rafRef = useRef(null); - const tasksContainerRef = useRef(null); - - const scheduleSetInsertIndex = (pos: number | null) => { - if (insertIndexRef.current === pos) return; - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - rafRef.current = null; - } - rafRef.current = requestAnimationFrame(() => { - insertIndexRef.current = pos; - setInsertIndex(pos); - rafRef.current = null; - }); - }; - - // Local tasks state, initially set from external tasks - // const [tasks, setTasks] = useState(tasksForThisColumn); - // const tasks = useMemo(() => tasksForThisColumn, [tasksForThisColumn]); - // Local tasks state, initially set from external tasks - // console.log("Column.tsx : Data in tasks :", tasks); - const [localTasks, setLocalTasks] = useState(tasksForThisColumn); - // Detect external changes in tasksForThisColumn - useEffect(() => { - setLocalTasks(tasksForThisColumn); - }, [tasksForThisColumn]); - - // // Render tasks using the tasks passed from KanbanBoard - // useEffect(() => { - // if (allTasksExternal.Pending.length > 0 || allTasksExternal.Completed.length > 0) { - // columnSegregator(plugin, setTasks, activeBoardIndex, colType, columnData, allTasksExternal); - // } - // }, [colType, columnData, allTasksExternal]); - - const columnWidth = plugin.settings.data.globalSettings.columnWidth || '273px'; - // const activeBoardSettings = plugin.settings.data.boardConfigs[activeBoardIndex]; - - // Extra code to provide special data-types for theme support. - const tagColors = plugin.settings.data.globalSettings.tagColors; - const tagColorMap = new Map(tagColors.map((t) => [t.name, t])); - let tagData = tagColorMap.get(columnData?.coltag || ''); - if (!tagData) { - tagColorMap.forEach((tagColor, tagNameKey, mapValue) => { - const result = matchTagsWithWildcards(tagNameKey, columnData?.coltag || ''); - // console.log("Column.tsx : Matching tag result : ", { tagNameKey, columnTag: columnData?.coltag, result }); - // Return the first match found - if (result) tagData = tagColor; - }); - } - - // Determine whether an advanced filter is applied (used by header count UI) - const isAdvancedFilterApplied = !isRootFilterStateEmpty(columnData.filters); - - // If this column is requested to render header-only (used by swimlane top header), return just the header UI - if (headerOnly) { - return ( -
- {columnData.minimized ? ( -
openColumnMenu(evt)} aria-label={t("open-column-menu")}>{localTasks?.length ?? 0}
- ) : ( -
-
-
{columnData.name}
-
-
openColumnMenu(evt)} aria-label={t("open-column-menu")}> - {localTasks?.length ?? 0} -
-
- )} -
- ); - } - - async function handleMinimizeColumn() { - // const boardIndex = plugin.settings.data.boardConfigs.findIndex( - // (board: Board) => board.name === activeBoardData.name - // ); - const boardIndex = activeBoardData.index; - - if (boardIndex !== -1) { - // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. - // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - // (col: ColumnData) => col.id === columnData.id - // ); - const columnIndex = columnData.index - 1; - - if (columnIndex !== -1) { - // Set the minimized property to true - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].minimized = !plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].minimized; - - // Save the settings - await plugin.saveSettings(); - - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } - } - } - - /** - * Opens the column menu, which allows the user to sort and filter the tasks in the column, - * configure the column's sorting and filtering, and hide the column. - * - * @param {MouseEvent | React.MouseEvent} event - The event that triggered the menu - */ - function openColumnMenu(event: MouseEvent | React.MouseEvent) { - const columnMenu = new Menu(); - - columnMenu.addItem((item) => { - item.setTitle(t("sort-and-filter")); - item.setIsLabel(true); - }); - columnMenu.addItem((item) => { - item.setTitle(t("configure-column-sorting")); - item.setIcon("arrow-up-down"); - item.onClick(async () => { - // open sorting modal - const modal = new ConfigureColumnSortingModal( - plugin, - columnData, - (updatedColumnConfiguration: ColumnData) => { - // Update the column configuration in the board data - // const boardIndex = plugin.settings.data.boardConfigs.findIndex( - // (board: Board) => board.name === activeBoardData.name - // ); - const boardIndex = activeBoardData.index; - - if (boardIndex !== -1) { - // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. - // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - // (col: ColumnData) => col.id === columnData.id - // ); - const columnIndex = columnData.index - 1; - - if (columnIndex !== -1) { - // Update the column configuration - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex] = updatedColumnConfiguration; - - // Save the settings - plugin.saveSettings(); - - eventEmitter.emit('REFRESH_BOARD'); - } - } - }, - () => { - // onCancel callback - nothing to do - } - ); - modal.open(); - }); - }); - columnMenu.addItem((item) => { - item.setTitle(t("configure-column-filtering")); - item.setIcon("list-filter"); - item.onClick(async () => { - try { - // const boardIndex = plugin.settings.data.boardConfigs.findIndex( - // (board: Board) => board.name === activeBoardData.name - // ); - const boardIndex = activeBoardData.index; - // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. - // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - // (col: ColumnData) => col.id === columnData.id - // ); - const columnIndex = columnData.index - 1; - - if (Platform.isMobile || Platform.isMacOS) { - // If its a mobile platform, then we will open a modal instead of popover. - const filterModal = new ViewTaskFilterModal( - plugin, true, undefined, boardIndex, columnData.name, columnData.filters - ); - - // Set the close callback - mainly used for handling cancel actions - filterModal.filterCloseCallback = async (filterState) => { - if (filterState && boardIndex !== -1) { - if (columnIndex !== -1) { - // Update the column filters - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].filters = filterState; - - // Save the settings - await plugin.saveSettings(); - - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } - } - }; - - filterModal.open(); - } else { - // Get the position of the menu (approximate column position) - // Use CSS.escape to properly escape the selector value - const escapedTag = columnData.coltag ? CSS.escape(columnData.coltag) : ''; - const columnElement = document.querySelector(`[data-column-tag-name="${escapedTag}"]`) as HTMLElement; - const position = columnElement - ? { x: columnElement.getBoundingClientRect().left, y: columnElement.getBoundingClientRect().top + 40 } - : { x: 100, y: 100 }; // Fallback position - - // Create and show filter popover - // leafId is undefined for column filters (not tied to a specific leaf) - const popover = new ViewTaskFilterPopover( - plugin, - true, // forColumn is true - undefined, - boardIndex, - columnData.name, - columnData.filters - ); - - // Set up close callback to save filter state - popover.onClose = async (filterState?: RootFilterState) => { - if (filterState && boardIndex !== -1) { - if (columnIndex !== -1) { - // Update the column filters - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].filters = filterState; - - // Save the settings - await plugin.saveSettings(); - - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } - } - }; - - popover.showAtPosition(position); - } - } catch (error) { - bugReporterManagerInsatance.showNotice(1, "Error showing filter popover", String(error), "Column.tsx/column-menu/configure-conlum-filters"); - } - }); - }); - - columnMenu.addSeparator(); - - columnMenu.addItem((item) => { - item.setTitle(t("quick-actions")); - item.setIsLabel(true); - }); - columnMenu.addItem((item) => { - item.setTitle(t("hide-column")); - item.setIcon("eye-off"); - item.onClick(async () => { - // const boardIndex = plugin.settings.data.boardConfigs.findIndex( - // (board: Board) => board.name === activeBoardData.name - // ); - const boardIndex = activeBoardData.index; - - if (boardIndex !== -1) { - // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. - // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - // (col: ColumnData) => col.id === columnData.id - // ); - const columnIndex = columnData.index - 1; - - if (columnIndex !== -1) { - // Set the active property to false - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].active = false; - - // Save the settings - await plugin.saveSettings(); - - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } - } - }); - }); - - // Show minimize or maximize option based on current state - if (columnData.minimized) { - columnMenu.addItem((item) => { - item.setTitle(t("maximize-column")); - item.setIcon("panel-left-open"); - item.onClick(async () => { - await handleMinimizeColumn(); - }); - }); - } else { - columnMenu.addItem((item) => { - item.setTitle(t("minimize-column")); - item.setIcon("panel-left-close"); - item.onClick(async () => { - await handleMinimizeColumn(); - }); - }); - } - - // Use native event if available (React event has nativeEvent property) - columnMenu.showAtMouseEvent( - (event instanceof MouseEvent ? event : event.nativeEvent) - ); - } - - // ------------------------------------------------- - // ALL DRAG AND DROP RELATED FUNCTIONS - // ------------------------------------------------- - - /** - * Handles the drop event of a task in this column. - * Moves the task from its original position (dragIndex) to the new position (dropIndex). - * Updates the localTasks state and the columnData.tasksIdManualOrder if the column uses manualOrder. - * Clears the raf timer to prevent any pending raf calls. - * @param {React.DragEvent} e - The drag event. - * @param {number} dropIndex - The index at which to drop the task. - */ - const handleTaskDrop = async (e: React.DragEvent, dropIndex: number) => { - e.preventDefault(); - setIsDragOver(false); - setInsertIndex(null); - - const targetColumnContainer = tasksContainerRef.current; - if (!targetColumnContainer) { - return; - } - - // We are basically doing same thing from the handleDrop function below. - dragDropTasksManagerInsatance.handleDropEvent( - e.nativeEvent, - columnData, - targetColumnContainer, - swimlaneData - ); - - // Clear manager payload (drag finished) - dragDropTasksManagerInsatance.clearCurrentDragData(); - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - - // const dragIndex = parseInt(e.dataTransfer.getData('text/plain')); - // if (isNaN(dragIndex) || dragIndex === dropIndex) return; - // const updated = [...localTasks]; - // const [moved] = updated.splice(dragIndex, 1); - // updated.splice(dropIndex, 0, moved); - // setLocalTasks(updated); - // // If this column uses manualOrder, update the columnData.tasksIdManualOrder to reflect new order - // const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - // if (hasManualOrder) { - // columnData.tasksIdManualOrder = updated.map(t => t.id); - // } - - // clear any pending raf - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - rafRef.current = null; - } - }; - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - - try { - // Get the data of the dragged task -- No need anymore, since its already stored in the dragdropmanager. - // const taskData = e.dataTransfer.getData('application/json'); - // if (taskData) { - // const { task, sourceColumnData } = JSON.parse(taskData); - - // // Ensure we have valid data - // if (!task || !sourceColumnData) return; - - // Get the target column container - const targetColumnContainer = (e.currentTarget) as HTMLDivElement; - - - // Try to locate the source container by stable column id first (works for all colTypes) -- No need to find this anymore, since I am not making use of sourceColumnContainer in dragdropmanager. - // let sourceColumnContainer: HTMLDivElement | null = null; - // if (sourceColumnData?.id) { - // try { - // const escapedId = CSS.escape(String(sourceColumnData.id)); - // sourceColumnContainer = document.querySelector(`.TaskBoardColumnsSection[data-column-id="${escapedId}"]`) as HTMLDivElement | null; - // } catch (err) { - // // fallback to tag-based lookup below - // } - // } - // if (!sourceColumnContainer) { - // // Fallback: find by tag name (legacy behavior) - // console.log("------------- I hope this fall-back mechanism is never running -------------"); - // const allColumnContainers = Array.from(document.querySelectorAll('.TaskBoardColumnsSection')) as HTMLDivElement[]; - // sourceColumnContainer = allColumnContainers.find(container => { - // const containerTag = container.getAttribute('data-column-tag-name'); - // return containerTag === sourceColumnData.coltag || sourceColumnData.coltag?.includes(containerTag || ''); - // }) || targetColumnContainer; - // } - - // we will allow cross-column drops now with target column having manualOrder sortCriteria. Disabling below code. - // const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - // if (hasManualOrder && sourceColumnData.id !== columnData.id) { - // // Not allowed: ignore drop - // dragDropTasksManagerInsatance.clearCurrentDragData(); - // dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // return; - // } - - // // Use the DragDropTasksManager to handle the drop - // try { - // const dragIdxStr = e.dataTransfer.getData('text/plain'); - // const dragIdx = dragIdxStr ? parseInt(dragIdxStr) : NaN; - // if (sourceColumnData.coltag === columnData.coltag && !isNaN(dragIdx) && insertIndexRef.current !== null) { - // // Reorder locally - // const updated = [...localTasks]; - // const [moved] = updated.splice(dragIdx, 1); - // updated.splice(insertIndexRef.current!, 0, moved); - // setLocalTasks(updated); - // setInsertIndex(null); - // insertIndexRef.current = null; - // // Update manual order if applicable - // const hasManualOrderLocal = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - // if (hasManualOrderLocal) { - // columnData.tasksIdManualOrder = updated.map(t => t.id); - // } - // // Clear manager payload and skip default handling - // dragDropTasksManagerInsatance.clearCurrentDragData(); - // dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // return; - // } - // } catch (err) { - // // ignore and continue to default handling - // } - - dragDropTasksManagerInsatance.handleDropEvent( - e.nativeEvent, - columnData, - targetColumnContainer, - swimlaneData - ); - - // Clear manager payload (drag finished) - dragDropTasksManagerInsatance.clearCurrentDragData(); - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // } - } catch (error) { - console.error('Error handling task drop:', error); - } - }, [columnData, plugin]); - - // This function will be only run when user will drag the taskItem on another taskItem. - // Compute insertion index based on mouse Y relative to task items inside the container. - const handleTaskItemDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - try { - // Only compute insertion index for columns that use "manualOrder" as the sorting criteria. - const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - if (!hasManualOrder) { - // Clear any visual placeholder and desired index - if (insertIndexRef.current !== null) { - scheduleSetInsertIndex(null); - } - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - return; - } else { - // APPROACH 1 - COMPUTE INSERTION INDEX BASED ON MOUSE Y POSITION BY COMPARING WITH TASK ITEM BOUNDING RECTANGLES - // Else will proceed with finding the insertion index - // const container = e.currentTarget.parentElement as HTMLDivElement; - // const children = Array.from(container.querySelectorAll('.taskItemFadeIn')) as HTMLElement[]; - // let pos = children.length; // default to end - // const clientY = e.clientY; - // for (let i = 0; i < children.length; i++) { - // const child = children[i]; - // const rect = child.getBoundingClientRect(); - // const midpoint = rect.top + rect.height / 2; - // if (clientY < midpoint) { - // pos = i; - // break; - // } - // } - - // APPROACH 2 - DIRECTLY FETCH THE INDEX FROM THE DATA ATTRIBUTE OF THE HOVERED ELEMENT - let pos = 0 // Default to top of the column - const hoveredElement = e.currentTarget; - const draggedOverItemIndex = hoveredElement.getAttribute('data-taskitem-index'); - const draggedOverItemKey = hoveredElement.getAttribute('data-taskitem-id'); - const draggedItemKey = dragDropTasksManagerInsatance.getCurrentDragData()?.task.id; - // console.log('handleTaskItemDragOver... \ndataAttribute', draggedOverItemIndex, "\ndraggedItemIndex", draggedItemIndex); - if (draggedOverItemKey && draggedOverItemIndex && draggedOverItemKey !== draggedItemKey) { - const clientY = e.clientY; - const rect = hoveredElement.getBoundingClientRect(); - const midpoint = rect.top + rect.height / 2; - if (clientY < midpoint) { - pos = parseInt(draggedOverItemIndex, 10); - } else { - pos = parseInt(draggedOverItemIndex, 10) + 1; - } - - // Throttle updates via RAF - scheduleSetInsertIndex(pos); - // Store desired drop index in manager - dragDropTasksManagerInsatance.setDesiredDropIndex(pos); - } else { - // Clear any visual placeholder and desired index - if (insertIndexRef.current !== null) { - scheduleSetInsertIndex(null); - } - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - } - - - // // Use the DragDropTasksManager to handle the drag over (this sets classes and dropEffect) - // dragDropTasksManagerInsatance.handleDragOver( - // e.nativeEvent, - // columnData, - // container - // ); - - const targetColumnContainer = tasksContainerRef.current as HTMLDivElement; - dragDropTasksManagerInsatance.handleCardDragOverEvent(e.nativeEvent as DragEvent, e.currentTarget as HTMLDivElement, targetColumnContainer, columnData); - } - } catch (error) { - console.error('Error computing insert index:', error); - } - }, [scheduleSetInsertIndex, columnData]); - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - try { - // // Try to read payload from the DataTransfer first - // let taskDataStr = ''; - // try { - // taskDataStr = e.dataTransfer.getData('application/json'); - // } catch (err) { - // // ignore - some environments restrict access - // } - - // let payload: any = null; - // if (taskDataStr) { - // try { payload = JSON.parse(taskDataStr); } catch { } - // } - - // // Fallback to manager-stored payload if dataTransfer is empty - // if (!payload) { - // payload = dragDropTasksManagerInsatance.getCurrentDragData(); - // } - - // if (!payload) return; - - // const { task, sourceColumnData } = payload; - // if (!task || !sourceColumnData) return; - - // Get the target column container - const targetColumnContainer = (e.currentTarget) as HTMLDivElement; - - // // Try id-based lookup first - // let sourceColumnContainer: HTMLDivElement | null = null; - // if (sourceColumnData?.id) { - // try { - // const escapedId = CSS.escape(String(sourceColumnData.id)); - // sourceColumnContainer = document.querySelector(`.TaskBoardColumnsSection[data-column-id="${escapedId}"]`) as HTMLDivElement | null; - // } catch (err) { - // // ignore and fall back to tag-based lookup - // } - // } - // if (!sourceColumnContainer) { - // const allColumnContainers = Array.from(document.querySelectorAll('.TaskBoardColumnsSection')) as HTMLDivElement[]; - // sourceColumnContainer = allColumnContainers.find(container => { - // const containerTag = container.getAttribute('data-column-tag-name'); - // return containerTag === sourceColumnData.coltag || sourceColumnData.coltag?.includes(containerTag || ''); - // }) || targetColumnContainer; - // } - - // Use the DragDropTasksManager to handle the drag over (this sets classes and dropEffect) - dragDropTasksManagerInsatance.handleColumnDragOverEvent( - e.nativeEvent, - columnData, - targetColumnContainer - ); - - // Below code is not required, since, I will call the dragDropTasksManagerInsatance.handleCardDragOverEvent from handleTaskItemDragOver. - // // If hovering over an actual card element, show card drop indicator - // try { - // const hovered = (e.target as HTMLElement).closest('.taskItem') as HTMLElement | null; - // if (hovered) { - // dragDropTasksManagerInsatance.handleCardDragOverEvent(e.nativeEvent as DragEvent, hovered); - // } - // } catch (err) { - // // ignore - // } - - // // Ensure cursor reflects allowed/not-allowed (best-effort fallback) - // const allowed = dragDropTasksManagerInsatance.isTaskDropAllowed(sourceColumnData, columnData); - // e.dataTransfer!.dropEffect = allowed ? 'move' : 'none'; - } catch (error) { - console.error('Error handling drag over:', error); - } - }, [columnData]); - - // Cleanup any pending RAF on unmount - useEffect(() => { - return () => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - rafRef.current = null; - } - }; - }, []); - - // Handle the dragleave event to remove the visual effect - const handleDragLeave = useCallback((e: React.DragEvent) => { - // Avoid flicker: if the drag event indicates the pointer is still within the container bounds, - // ignore this dragleave (this happens when moving between child elements). - try { - const container = e.currentTarget as HTMLElement; - const x = e.clientX; - const y = e.clientY; - if (typeof x === 'number' && typeof y === 'number') { - const rect = container.getBoundingClientRect(); - if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { - // still inside container — ignore to prevent CSS flicker - return; - } - } - } catch (err) { - // ignore and continue cleanup - } - - setIsDragOver(false); - setInsertIndex(null); - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // Let manager clean up the dropindicator and column highlight - dragDropTasksManagerInsatance.handleDragLeaveEvent(e.currentTarget as HTMLDivElement); - }, []); - - // ------------------------------------------------- - // Render - // ------------------------------------------------- - - return ( -
- {columnData.minimized && !hideColumnHeader ? ( - // Minimized view - vertical bar with count and rotated text -
-
openColumnMenu(evt)} aria-label={t("open-column-menu")}> - {localTasks.length} -
-
{ - await handleMinimizeColumn(); - eventEmitter.emit('REFRESH_BOARD'); - }}>{columnData.name}
-
- ) : ( - // Normal view - <> - {!hideColumnHeader && ( -
-
-
{columnData.name}
-
-
openColumnMenu(evt)} aria-label={t("open-column-menu")}> - {localTasks?.length ?? 0} -
-
- )} -
{ handleDragOver(e); }} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - onDragEnd={(e) => { setIsDragOver(false); setInsertIndex(null); dragDropTasksManagerInsatance.clearAllDragStyling(); }} - > - {columnData.minimized ? <> : ( - <> - {localTasks.length > 0 ? ( - (() => { - const elements: React.ReactNode[] = []; - for (let i = 0; i < localTasks.length; i++) { - // If insertIndex points to this position, render placeholder - if (insertIndex === i) { - elements.push( -
Drop here
- ); - } - const task = localTasks[i]; - elements.push( -
{ handleTaskItemDragOver(e); } - } - onDrop={e => handleTaskDrop(e, i)} - > - -
- ); - } - // If insertIndex points to end (after last item) - if (insertIndex === localTasks.length) { - elements.push( -
Drop here
- ); - } - return elements; - })() - ) : ( -

{t("no-tasks-available")}

- )} - - ) - } -
- - )} -
- ); - -}; - -// const MemoizedTaskItem = memo(TaskItem, (prevProps, nextProps) => { -// return ( -// prevProps.task.id === nextProps.task.id && // Immutable check -// prevProps.task.title === nextProps.task.title && -// prevProps.task.body === nextProps.task.body && -// prevProps.task.due === nextProps.task.due && -// prevProps.task.tags.join(",") === nextProps.task.tags.join(",") && -// prevProps.task.priority === nextProps.task.priority && -// prevProps.task.completed === nextProps.task.completed && -// prevProps.task.filePath === nextProps.task.filePath && -// prevProps.columnIndex === nextProps.columnIndex && -// prevProps.activeBoardSettings === nextProps.activeBoardSettings -// ); -// }); - -export default memo(Column); diff --git a/src/components/KanbanView/KanbanBoardView.tsx b/src/components/KanbanView/KanbanBoardView.tsx index ff2f81ce..5afc01a7 100644 --- a/src/components/KanbanView/KanbanBoardView.tsx +++ b/src/components/KanbanView/KanbanBoardView.tsx @@ -1,326 +1,162 @@ // src/components/KanbanBoard.tsx -import { Board } from "../../interfaces/BoardConfigs"; -import React, { memo } from "react"; -import { taskItem, taskJsonMerged } from "src/interfaces/TaskItem"; - -import { App } from "obsidian"; -import Column from "./Column"; -import LazyColumn from "./LazyColumn"; -import KanbanSwimlanesContainer from "./KanbanSwimlanesContainer"; -import type TaskBoard from "main"; -import { t } from "src/utils/lang/helper"; +import { t } from "i18next"; +import React, { memo, useMemo, useState, useEffect } from "react"; +import TaskBoard from "../../../main.js"; +import type { Board, ColumnData, TaskBoardViewType } from "../../interfaces/BoardConfigs.js"; +import type { taskJsonMerged, taskItem } from "../../interfaces/TaskItem.js"; +import { columnSegregator } from "../../utils/algorithms/ColumnSegregator.js"; +import KanbanSwimlanesContainer from "./KanbanSwimlanesContainer.js"; +import LazyColumn from "./LazyColumn.js"; interface KanbanBoardProps { - app: App; plugin: TaskBoard; - board: Board; - allTasks: taskJsonMerged | undefined; - tasksPerColumn: taskItem[][]; - loading: boolean; + currentBoardData: Board; + currentView: TaskBoardViewType; + currentViewIndex: number; + filteredAndSearchedTasks: taskJsonMerged; freshInstall: boolean; } -const KanbanBoard: React.FC = ({ plugin, board, allTasks, tasksPerColumn, loading, freshInstall }) => { - // Check if lazy loading is enabled - const lazyLoadingEnabled = plugin.settings.data.globalSettings.kanbanView?.lazyLoadingEnabled ?? false; - const ColumnComponent = lazyLoadingEnabled ? LazyColumn : Column; +const KanbanBoard: React.FC = ({ plugin, currentBoardData, currentView, currentViewIndex, filteredAndSearchedTasks, freshInstall }) => { + const [loading, setLoading] = useState(true); + + // const ColumnComponent = LazyColumn; // lazyLoadingEnabled ? LazyColumn : Column; + const columns = currentView?.kanbanView?.columns || []; + + // Second memo: Segregate filtered tasks by column (for Kanban view only) + const allTasksArrangedPerColumn = useMemo(() => { + if (currentBoardData && currentView && filteredAndSearchedTasks) { + return columns + .filter((column) => column.active) + .map((column: ColumnData) => + columnSegregator(plugin.settings, currentView, column, filteredAndSearchedTasks, (updatedViewData: TaskBoardViewType) => { + let updatedBoardData = { ...currentBoardData }; + if (updatedBoardData.views) { + updatedBoardData.views[currentViewIndex] = updatedViewData; + } + + plugin.taskBoardFileManager.debouncedSaveBoard(updatedBoardData); + }) + ); + } + return []; + }, [filteredAndSearchedTasks, currentBoardData, currentView, columns, currentViewIndex, plugin]); + + useEffect(() => { + setLoading(!(currentBoardData && currentView && filteredAndSearchedTasks)); + }, [currentBoardData, currentView, filteredAndSearchedTasks]); + + if (!currentView?.kanbanView) { + return ( +
+ {t("Looks like the Kanban view data has been currupted. Please try duplicating this view or create a fresh new view.")} +
+ ) + } + + const renderColumns = (columns: ColumnData[], tasks: taskItem[][]) => { + return columns.map((column, index) => ( + + )); + }; + + const renderLoadingOrEmpty = useMemo(() => { + if (loading) { + return ( +
+
+

{t("loading-tasks")}

+
+ ); + } + + if (currentView.kanbanView!.columns?.length === 0) { + return ( +
+ {t("no-columns-message")} +
+ ); + } + + return null; + }, [loading]); + + const renderFreshInstallMessage = useMemo(() => { + if (freshInstall) { + return ( +
+

+ {t("fresh-install-1")} +
+
+ {t("fresh-install-2")} +
+
+ {t("fresh-install-3")} +

+
+ ); + } + + return null; + }, [freshInstall]); + + const isSwimlanesEnabled = currentView.kanbanView!.swimlanes?.enabled === true; return (
-
- {loading ? ( -
- {freshInstall ? ( -

- {t("fresh-install-1")} -
-
- {t("fresh-install-2")} -
-
- {t("fresh-install-3")} -

- ) : ( - <> -
-

{t("loading-tasks")}

- - )} -
- ) : board?.columns?.length === 0 ? ( -
- Create columns on this board using the board config modal from top right corner button. -
- ) : board?.swimlanes?.enabled ? ( - - ) : ( - board?.columns - .filter((column) => column.active) - .map((column, index) => ( - - )) - )} -
+ {renderLoadingOrEmpty || renderFreshInstallMessage || ( + <> + {isSwimlanesEnabled ? ( + + ) : ( +
+ {renderColumns( + columns.filter((column) => column.active) || [], + allTasksArrangedPerColumn + )} +
+ )} + + )}
); }; -const MemoizedColumn = memo<{ - plugin: TaskBoard; - columnIndex: number; - activeBoardData: Board; - columnData: any; - tasksForThisColumn: taskItem[]; - Component: typeof Column | typeof LazyColumn; -}>(({ Component, ...props }) => { - return ; -}, (prevProps, nextProps) => { - return ( - prevProps.tasksForThisColumn === nextProps.tasksForThisColumn && - prevProps.columnData === nextProps.columnData && - prevProps.Component === nextProps.Component - ); -}); - -export default memo(KanbanBoard); - - - - - - - - - - -// // src/components/KanbanBoard.tsx - V1 - -// import { Board, ColumnData } from "../interfaces/BoardConfigs"; -// import { Bolt, CirclePlus, RefreshCcw, Tally1 } from 'lucide-react'; -// import React, { memo, useCallback, useEffect, useMemo, useState } from "react"; -// import { loadBoardsData, loadTasksAndMerge } from "src/utils/JsonFileOperations"; -// import { taskJsonMerged } from "src/interfaces/TaskItem"; - -// import { App } from "obsidian"; -// import Column from "./Column"; -// import type TaskBoard from "main"; -// import debounce from "debounce"; -// import { eventEmitter } from "src/services/EventEmitter"; -// import { handleUpdateBoards } from "../utils/BoardOperations"; -// import { bugReporter, openAddNewTaskModal, openBoardConfigModal } from "../services/OpenModals"; -// import { columnSegregator } from 'src/utils/RenderColumns'; -// import { t } from "src/utils/lang/helper"; - -// const KanbanBoard: React.FC<{ app: App, plugin: TaskBoard, boardConfigs: Board[] }> = ({ app, plugin, boardConfigs }) => { -// const [boards, setBoards] = useState(boardConfigs); -// const [activeBoardIndex, setActiveBoardIndex] = useState(0); -// const [allTasks, setAllTasks] = useState(); -// // const [allTasksArrangedPerColumn, setAllTasksArrangedPerColumn] = useState([]); -// const [refreshCount, setRefreshCount] = useState(0); -// const [loading, setLoading] = useState(true); -// const [freshInstall, setFreshInstall] = useState(false); - -// useEffect(() => { -// const fetchData = async () => { -// try { -// const data = await loadBoardsData(plugin); -// setBoards(data); - -// const allTasks = await loadTasksAndMerge(plugin); -// // console.log("KanbanBoard.tsx : Data in allTasks :", allTasks); -// if (allTasks) { -// setAllTasks(allTasks); -// setFreshInstall(false); -// } -// } catch (error) { -// setFreshInstall(true); -// // bugReporterManagerInsatance.showNotice(2, "Error loading boards or tasks data", error as string, "KanbanBoard.tsx/useEffect"); -// } -// }; - -// fetchData(); -// // fetchData().finally(() => setLoading(false)); -// }, [refreshCount]); - -// const allTasksArrangedPerColumn = useMemo(() => { -// if (allTasks && boards[activeBoardIndex]) { -// return boards[activeBoardIndex].columns -// .filter((column) => column.active) -// .map((column: ColumnData) => -// columnSegregator(plugin, activeBoardIndex, column, allTasks) -// ); -// } -// return []; -// }, [allTasks, boards, activeBoardIndex]); - -// useEffect(() => { -// if (allTasksArrangedPerColumn.length > 0) { -// setLoading(false); -// } -// }, [allTasksArrangedPerColumn]); - - -// // // Load tasks only once when the board is refreshed -// // useEffect(() => { -// // refreshBoardData(setBoards, async () => { -// // try { -// // const data = await loadBoardsData(plugin); // Fetch updated board data -// // setBoards(data); // Update the state with the new data -// // const allTasks = await loadTasksAndMerge(plugin); -// // console.log("KanbanBoard.tsx : Data in allTasks :", allTasks); -// // if (allTasks) { -// // setAllTasks(allTasks); -// // } -// // } catch (error) { -// // console.error("refreshBoardData : Error loading tasks:", error); -// // } -// // }); -// // }, []); - -// // useEffect(() => { -// // if (allTasks && boards[activeBoardIndex]) { -// // const columns = boards[activeBoardIndex].columns; -// // const arrangedTasks = columns.map((column: ColumnData) => { -// // return columnSegregator(plugin, activeBoardIndex, column, allTasks); -// // }); -// // console.log("KanbanBoard.tsx : Data in setAllTasksArrangedPerColumn:", arrangedTasks); -// // setAllTasksArrangedPerColumn(arrangedTasks); -// // } -// // }, [allTasks, boards, activeBoardIndex]); - -// const debouncedRefreshColumn = useCallback(debounce(async () => { -// try { -// const allTasks = await loadTasksAndMerge(plugin); -// setAllTasks(allTasks); -// } catch (error) { -// bugReporterManagerInsatance.showNotice(3, "Error loading tasks on column refresh", error as string, "KanbanBoard.tsx/debouncedRefreshColumn"); -// } -// }, 300), [plugin]); - -// useEffect(() => { -// eventEmitter.on('REFRESH_COLUMN', debouncedRefreshColumn); -// return () => { -// eventEmitter.off('REFRESH_COLUMN', debouncedRefreshColumn); -// }; -// }, [debouncedRefreshColumn]); - -// // Pub Sub method similar to Kafka to read events/messages. -// useEffect(() => { -// const refreshBoardListener = () => { -// // Clear the tasks array -// // setAllTasks(undefined); -// setRefreshCount((prev) => prev + 1); -// }; - -// // const refreshColumnListener = async () => { -// // try { -// // const allTasks = await loadTasksAndMerge(plugin); -// // // setAllTasksArrangedPerColumn([]); -// // setAllTasks(allTasks); -// // } catch (error) { -// // console.error("Error loading tasks:", error); -// // } -// // }; - -// eventEmitter.on('REFRESH_BOARD', refreshBoardListener); -// // eventEmitter.on('REFRESH_COLUMN', refreshColumnListener); - -// // Clean up the listener when component unmounts -// return () => { -// eventEmitter.off('REFRESH_BOARD', refreshBoardListener); -// // eventEmitter.off('REFRESH_COLUMN', refreshColumnListener); -// }; -// }, []); - -// // Memoized refreshBoardButton to avoid re-creating the function on every render -// const refreshBoardButton = useCallback(async () => { -// if (plugin.settings.data.globalSettings.realTimeScanner) { -// eventEmitter.emit("REFRESH_BOARD"); -// } else { -// if ( -// localStorage.getItem(PENDING_SCAN_FILE_STACK)?.at(0) !== undefined -// ) { -// await plugin.realTimeScanner.processAllUpdatedFiles(); -// } -// eventEmitter.emit("REFRESH_BOARD"); -// } -// }, [plugin]); - -// function handleOpenAddNewTaskModal() { -// openAddNewTaskModal(app, plugin); -// } - -// // const isLoading = !boards[activeBoardIndex]?.columns.every( -// // (_, index) => allTasksArrangedPerColumn[index]?.length > 0 -// // ); - -// // // If you prefer a more robust check that verifies whether the data is not only populated but also corresponds correctly to the columns: -// // const isLoading = -// // allTasksArrangedPerColumn.length !== boards[activeBoardIndex]?.columns.length || -// // allTasksArrangedPerColumn.some((tasks) => tasks.length === 0); - -// return ( -//
-//
-// {loading ? ( -//
-// {freshInstall ? ( -//

-// {t("fresh-install-1")} -//
-//
-// {t("fresh-install-2")} -//
-//
-// {t("fresh-install-3")} -//

-// ) : ( -// <> -//
-//

{t('loading-tasks')}

-// -// )} -//
-// ) : ( -// boards[activeBoardIndex]?.columns -// .filter((column) => column.active) -// .map((column, index) => ( -// -// )) -// )} -//
-//
-// ); -// }; - -// // Wrap Column in React.memo -// const MemoizedColumn = memo(Column, (prevProps, nextProps) => { +// const MemoizedColumn = memo<{ +// plugin: TaskBoard; +// activeBoardData: Board; +// currentViewIndex: number; +// kanbanViewData: KanbanView; +// columnData: ColumnData; +// tasksForThisColumn: taskItem[]; +// Component: typeof LazyColumn; +// }>(({ Component, ...props }) => { +// return ; +// }, (prevProps, nextProps) => { // return ( +// prevProps.activeBoardData === nextProps.activeBoardData && +// prevProps.currentViewIndex === nextProps.currentViewIndex && +// prevProps.kanbanViewData === nextProps.kanbanViewData && +// prevProps.columnData === nextProps.columnData && // prevProps.tasksForThisColumn === nextProps.tasksForThisColumn && -// prevProps.columnData === nextProps.columnData +// prevProps.Component === nextProps.Component // ); // }); -// export default memo(KanbanBoard); +export default memo(KanbanBoard); diff --git a/src/components/KanbanView/KanbanSwimlanesContainer.tsx b/src/components/KanbanView/KanbanSwimlanesContainer.tsx index e521d2ce..8795c8bb 100644 --- a/src/components/KanbanView/KanbanSwimlanesContainer.tsx +++ b/src/components/KanbanView/KanbanSwimlanesContainer.tsx @@ -1,21 +1,24 @@ // src/components/KanbanView/KanbanSwimlanesContainer.tsx +import { t } from 'i18next'; +import { TableCellsSplit, ChevronRight, ChevronDown } from 'lucide-react'; import React, { useMemo, memo } from 'react'; -import { Board, ColumnData } from 'src/interfaces/BoardConfigs'; -import { taskItem, taskJsonMerged } from 'src/interfaces/TaskItem'; -import Column from './Column'; -import LazyColumn from './LazyColumn'; -import type TaskBoard from 'main'; -import { t } from 'src/utils/lang/helper'; -import { ChevronDown, ChevronLast, ChevronLeft, ChevronRight } from 'lucide-react'; -import { eventEmitter } from 'src/services/EventEmitter'; +import TaskBoard from '../../../main.js'; +import { Board, KanbanView, ColumnData } from '../../interfaces/BoardConfigs.js'; +import { HeaderUITypeOptions } from '../../interfaces/Enums.js'; +import { taskItem } from '../../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +import { eventEmitter } from '../../services/EventEmitter.js'; +import { getAllTaskTags } from '../../utils/TaskItemUtils.js'; +import LazyColumn from './LazyColumn.js'; +import { getStatusNameFromStatusSymbol } from '../../utils/taskNote/TaskNoteUtils.js'; interface KanbanSwimlanesContainerProps { plugin: TaskBoard; - board: Board; - allTasks: taskJsonMerged | undefined; + currentBoardData: Board; + currentViewIndex: number; + kanbanViewData: KanbanView; tasksPerColumn: taskItem[][]; - lazyLoadingEnabled: boolean; } interface SwimlaneRow { @@ -27,12 +30,47 @@ interface SwimlaneRow { const KanbanSwimlanesContainer: React.FC = ({ plugin, - board, - allTasks, + currentBoardData, + currentViewIndex, + kanbanViewData, tasksPerColumn, - lazyLoadingEnabled, }) => { - const ColumnComponent = lazyLoadingEnabled ? LazyColumn : Column; + // const ColumnComponent = LazyColumn; // lazyLoadingEnabled ? LazyColumn : Column; + + // Separate columns into swimlane-enabled and excluded + const { columnsInSwimlanes, columnsOutsideSwimlanes, swimlaneColumnTasks, outsideSwimlaneColumnTasks } = useMemo(() => { + const activeColumns = kanbanViewData.columns.filter((col) => col.active); + const outsideSwimlanes: ColumnData[] = []; + const insideSwimlanes: ColumnData[] = []; + const outsideTasks: taskItem[][] = []; + const insideTasks: taskItem[][] = []; + + activeColumns.forEach((column, index) => { + if (column.swimlaneEnabled === false) { + outsideSwimlanes.push(column); + outsideTasks.push(tasksPerColumn[index] || []); + } else { + insideSwimlanes.push(column); + insideTasks.push(tasksPerColumn[index] || []); + } + }); + + return { + columnsOutsideSwimlanes: outsideSwimlanes, + columnsInSwimlanes: insideSwimlanes, + outsideSwimlaneColumnTasks: outsideTasks, + swimlaneColumnTasks: insideTasks + }; + }, [kanbanViewData.columns, tasksPerColumn]); + + // Create a modified board for swimlanes with only swimlane-enabled columns + const swimlaneBoard = useMemo(() => { + if (columnsInSwimlanes.length === 0) return null; + return { + ...kanbanViewData, + columns: columnsInSwimlanes + }; + }, [kanbanViewData, columnsInSwimlanes]); // Extract and organize swimlanes using tasksPerColumn (already segregated per active column) const { @@ -42,26 +80,25 @@ const KanbanSwimlanesContainer: React.FC = ({ customValue, groupAllRest, maxHeight: maxSwimlaneHeight, - verticalHeaderUI, + headerUIType, minimized - } = board.swimlanes; + } = kanbanViewData.swimlanes; const swimlanes: SwimlaneRow[] = useMemo(() => { - if (!board.swimlanes?.enabled || !tasksPerColumn) { + if (!swimlaneColumnTasks || swimlaneColumnTasks.flat().length < 1) { return []; } - // Get all active columns - const activeColumns = board.columns.filter((col) => col.active); - if (activeColumns.length === 0) return []; + if (columnsInSwimlanes.length === 0) return []; // Extract unique values for the swimlane property from tasksPerColumn - const uniqueSwimlanValues = extractUniquePropertyValuesFromColumns( - tasksPerColumn, + let uniqueSwimlanValues = extractUniquePropertyValuesFromColumns( + swimlaneColumnTasks, property, customValue ); + uniqueSwimlanValues.push(""); // Sort the swimlane values let sortedSwimlaneValues: { value: string; index: number }[] = []; @@ -130,18 +167,18 @@ const KanbanSwimlanesContainer: React.FC = ({ // Create swimlane rows with tasks organized by column const swimlaneRows: SwimlaneRow[] = sortedSwimlaneValues.map((swimlaneItem) => { - const tasksByColumn = activeColumns.map((column, colIdx) => { - // tasksPerColumn is expected to align with active columns order - const columnTasks = tasksPerColumn[colIdx] || []; + const tasksByColumn = columnsInSwimlanes.map((column: ColumnData, colIdx: number) => { + // swimlaneColumnTasks is expected to align with active columns order + const columnTasks = swimlaneColumnTasks[colIdx] || []; // If this swimlane is the aggregated "All rest", include any task whose // property values are in the remainingValuesForAllRest list. if (swimlaneItem.value === 'All rest') { if (!remainingValuesForAllRest || remainingValuesForAllRest.length === 0) return []; return columnTasks.filter((task: taskItem) => { - const values = getPropertyValues(task, property, customValue); + let values = getPropertyValues(task, property, customValue); if (property === "tags") { - values.map((tag: string) => tag.replace('#', '').toLocaleLowerCase()); + values = values.map((tag: string) => tag.replace('#', '').toLocaleLowerCase()); const doesValuesHaveCustomValues = values.some((v: string) => customValues.has(v)); return values.some((v: string) => remainingValuesForAllRest.includes(v) && !doesValuesHaveCustomValues) || values.length === 0; } @@ -152,9 +189,9 @@ const KanbanSwimlanesContainer: React.FC = ({ // Default behavior: filter tasks that include the exact swimlane value return columnTasks.filter((task) => { - const values = getPropertyValues(task, property, customValue); + let values = getPropertyValues(task, property, customValue); if (property === "tags") { - values.map((tag: string) => tag.replace('#', '').toLocaleLowerCase()); + values = values.map((tag: string) => tag.replace('#', '').toLocaleLowerCase()); return values.includes(swimlaneItem.value.replace('#', '').toLocaleLowerCase()); } @@ -162,7 +199,8 @@ const KanbanSwimlanesContainer: React.FC = ({ }); }); - const swimlaneName = t(property) + ': ' + swimlaneItem.value; + const statusName = getStatusNameFromStatusSymbol(swimlaneItem.value, plugin.settings.data.customStatuses ?? []); + const swimlaneName = property === 'status' ? (statusName ? statusName : 'All rest') : swimlaneItem.value; const isSwimlaneMinimized = minimized?.includes(swimlaneName) ?? false; return { @@ -174,19 +212,36 @@ const KanbanSwimlanesContainer: React.FC = ({ }); // Filter out empty swimlanes if hideEmptySwimlanes is false - if (board.swimlanes.hideEmptySwimlanes) { + if (kanbanViewData.swimlanes?.hideEmptySwimlanes) { return swimlaneRows.filter((row) => row.tasks.some((columnTasks) => columnTasks.length > 0) ); } return swimlaneRows; - }, [board, tasksPerColumn, plugin]); - - if (swimlanes.length === 0) { + }, [currentBoardData, tasksPerColumn, plugin]); + + const hasExcludedColumns = columnsOutsideSwimlanes.length > 0; + const hasSwimlaneColumns = columnsInSwimlanes.length > 0; + + const renderExcludedColumns = () => { + return columnsOutsideSwimlanes.map((column, index) => ( + + )); + }; + + if (swimlanes.length === 0 && !hasExcludedColumns) { return (
- {t('no-swimlanes-found') || 'No swimlanes found for this configuration.'} + {t('no-swimlanes-found-message')}
); } @@ -224,137 +279,171 @@ const KanbanSwimlanesContainer: React.FC = ({ // ); // } - const activeColumns = board.columns.filter((col) => col.active); - async function handleSwimlaneMinimize(rowIndex: number) { try { const swimlaneName = swimlanes[rowIndex]?.swimlaneName; if (!swimlaneName) return; - const boardIndex = board.index; // plugin.settings.data.boardConfigs.findIndex((b) => b.index === board.index); - if (boardIndex === -1) return; - const swimCfg = plugin.settings.data.boardConfigs[boardIndex].swimlanes || { minimized: [] }; + // const boardIndex = currentBoardData.index; // plugin.settings.data.boardConfigs.findIndex((b) => b.index === currentBoardData.index); + // if (boardIndex === -1) return; + const swimCfg = kanbanViewData.swimlanes || { minimized: [] }; const arr = Array.isArray(swimCfg.minimized) ? [...swimCfg.minimized] : []; const idx = arr.indexOf(swimlaneName); if (idx === -1) arr.push(swimlaneName); else arr.splice(idx, 1); - plugin.settings.data.boardConfigs[boardIndex].swimlanes.minimized = arr; - await plugin.saveSettings(); + + // Create updated view data with new swimlanes configuration + const updatedKanbanViewData: KanbanView = { + ...kanbanViewData, + swimlanes: { + ...kanbanViewData.swimlanes, + minimized: arr, + }, + }; + + const updatedBoardData = { ...currentBoardData }; + if (updatedBoardData.views[currentViewIndex].kanbanView) { + updatedBoardData.views[currentViewIndex].kanbanView = updatedKanbanViewData; + } + + + await plugin.taskBoardFileManager.debouncedSaveBoard(updatedBoardData); eventEmitter.emit('REFRESH_BOARD'); } catch (err) { - console.error('Error toggling swimlane minimize:', err); + bugReporterManagerInsatance.addToLogs( + 121, + String(err), + "KanbanSwimlanesContainer.tsx/handleSwimlaneMinimize", + ); } } // Render a sticky header row of column headers across swimlanes return (
+ {/* Columns excluded from swimlanes (rendered on the left) */} + {hasExcludedColumns && ( +
+ {renderExcludedColumns()} +
+ )} {/* Swimlane Rows */} -
- {/* Top header showing column headers and counts */} -
-
- {activeColumns.map((column, colIndex) => ( - - ))} + {hasSwimlaneColumns && swimlaneBoard && ( +
+ {/* Top header showing column headers and counts */} +
+ {/* A small Icon at the top right corner inside the swimlanes container */} + {headerUIType === HeaderUITypeOptions.vertical && ( + + )} + +
+ {columnsInSwimlanes.map((column: ColumnData, colIndex: number) => ( + + ))} +
-
- {swimlanes.map((swimlane, rowIndex) => ( - - {verticalHeaderUI ? ( -
- {/* Swimlane Label */} -
-
-
- {swimlane.tasks.flat().length ?? 0} -
-
- {swimlane.swimlaneName} -
-
handleSwimlaneMinimize(rowIndex)}> - {swimlane.minimized ? () : ()} + {swimlanes.map((swimlane, rowIndex) => ( + + {headerUIType === HeaderUITypeOptions.vertical ? ( +
+ {/* Swimlane Label */} +
+
+
+ {swimlane.tasks.flat().length ?? 0} +
+
+
{property}:
+
{swimlane.swimlaneName}
+
+
handleSwimlaneMinimize(rowIndex)}> + +
-
- {/* Columns for this Swimlane */} -
- {swimlane.minimized ? null : activeColumns.map((column, colIndex) => { - const swimlaneData = { - property: board.swimlanes.property, - value: swimlane.swimlaneValue, - }; - - return ( - - ); - })} + {/* Columns for this Swimlane */} +
+ {swimlane.minimized ? null : columnsInSwimlanes.map((column: ColumnData, colIndex: number) => { + const swimlaneData = { + property: kanbanViewData.swimlanes.property, + value: swimlane.swimlaneValue, + }; + + return ( + + ); + })} +
-
- ) : ( -
- {/* Swimlane Label */} -
-
-
handleSwimlaneMinimize(rowIndex)}> - {swimlane.minimized ? () : ()} -
-
- {swimlane.swimlaneName} -
-
- {swimlane.tasks.flat().length ?? 0} + ) : ( +
+ {/* Swimlane Label */} +
+
+
+
handleSwimlaneMinimize(rowIndex)}> + {swimlane.minimized ? () : ()} +
+
+
{property}:
+
{swimlane.swimlaneName}
+
+
+
+ {swimlane.tasks.flat().length ?? 0} +
-
- {/* Columns for this Swimlane */} -
- {swimlane.minimized ? null : activeColumns.map((column, colIndex) => { - const swimlaneData = { - property: board.swimlanes.property, - value: swimlane.swimlaneValue, - }; - - return ( - - ); - })} + {/* Columns for this Swimlane */} +
+ {swimlane.minimized ? null : columnsInSwimlanes.map((column: ColumnData, colIndex: number) => { + const swimlaneData = { + property: kanbanViewData.swimlanes.property || 'tags', + value: swimlane.swimlaneValue, + }; + + return ( + + ); + })} +
-
- )} - - ))} -
+ )} + + ))} +
+ )}
); }; @@ -393,16 +482,17 @@ function getPropertyValues( switch (property) { case 'tags': - if (task.tags && Array.isArray(task.tags)) { - values = task.tags.map((tag: string) => { + const allTags = getAllTaskTags(task); + if (allTags && allTags.length > 0) { + values = allTags.map((tag: string) => { if (typeof tag === 'string') return tag.replace('#', ''); return ''; - }).filter((v: string) => v); + }).filter((v: string) => v.trim()); } break; case 'priority': - if (task.priority !== undefined && task.priority !== null) { + if (typeof task.priority === 'number') { values = [String(task.priority)]; } break; @@ -441,31 +531,35 @@ function getPropertyValues( break; } - return values.filter((v) => v && v.trim()); + return values; } /** * Memoized swimlane column component */ -const MemoizedSwimlanColumn = memo<{ - plugin: TaskBoard; - columnIndex: number; - activeBoardData: Board; - columnData: ColumnData; - tasksForThisColumn: taskItem[]; - Component: typeof Column | typeof LazyColumn; - hideColumnHeader?: boolean; - swimlaneData?: { property: string, value: string }; - headerOnly?: boolean; -}>(({ Component, ...props }) => { - return ; -}, (prevProps, nextProps) => { - return ( - prevProps.tasksForThisColumn === nextProps.tasksForThisColumn && - prevProps.columnData === nextProps.columnData && - prevProps.Component === nextProps.Component && - prevProps.hideColumnHeader === nextProps.hideColumnHeader - ); -}); +// const MemoizedSwimlanColumn = memo<{ +// plugin: TaskBoard; +// activeBoardData: Board; +// kanbanViewData: KanbanView; +// currentViewIndex: number; +// columnData: ColumnData; +// tasksForThisColumn: taskItem[]; +// Component: typeof LazyColumn; +// swimlaneData?: { property: string, value: string }; +// hideColumnHeader?: boolean; +// headerOnly?: boolean; +// }>(({ Component, ...props }) => { +// return ; +// }, (prevProps, nextProps) => { +// return ( +// prevProps.activeBoardData === nextProps.activeBoardData && +// prevProps.kanbanViewData === nextProps.kanbanViewData && +// prevProps.currentViewIndex === nextProps.currentViewIndex && +// prevProps.columnData === nextProps.columnData && +// prevProps.tasksForThisColumn === nextProps.tasksForThisColumn && +// prevProps.Component === nextProps.Component && +// prevProps.hideColumnHeader === nextProps.hideColumnHeader +// ); +// }); export default memo(KanbanSwimlanesContainer); diff --git a/src/components/KanbanView/LazyColumn.tsx b/src/components/KanbanView/LazyColumn.tsx index 7ed7d1db..2ebf16d6 100644 --- a/src/components/KanbanView/LazyColumn.tsx +++ b/src/components/KanbanView/LazyColumn.tsx @@ -1,26 +1,24 @@ // src/components/KanbanView/LazyColumn.tsx import React, { memo, useMemo, useState, useEffect, useRef, useCallback } from 'react'; - import { CSSProperties } from 'react'; -import TaskItem, { swimlaneDataProp } from './TaskItem'; -import { t } from 'src/utils/lang/helper'; -import TaskBoard from 'main'; -import { Board, ColumnData, RootFilterState } from 'src/interfaces/BoardConfigs'; -import { taskItem } from 'src/interfaces/TaskItem'; import { Menu, Notice, Platform } from 'obsidian'; -import { ViewTaskFilterPopover } from 'src/components/BoardFilters/ViewTaskFilterPopover'; -import { eventEmitter } from 'src/services/EventEmitter'; -import { bugReporter } from 'src/services/OpenModals'; -import { ViewTaskFilterModal } from 'src/components/BoardFilters'; -import { ConfigureColumnSortingModal } from 'src/modals/ConfigureColumnSortingModal'; -import { matchTagsWithWildcards } from 'src/utils/algorithms/ScanningFilterer'; -import { isRootFilterStateEmpty } from 'src/utils/algorithms/BoardFilterer'; -import { dragDropTasksManagerInsatance } from 'src/managers/DragDropTasksManager'; -import { taskCardStyleNames } from 'src/interfaces/GlobalSettings'; -import TaskItemV2 from './TaskItemV2'; +import { t } from 'i18next'; import { AlertOctagon } from 'lucide-react'; -import { bugReporterManagerInsatance } from 'src/managers/BugReporter'; +import TaskBoard from '../../../main.js'; +import { Board, KanbanView, ColumnData, RootFilterState } from '../../interfaces/BoardConfigs.js'; +import { taskCardStyleNames, viewTypeNames } from '../../interfaces/Enums.js'; +import { taskItem } from '../../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +import { dragDropTasksManagerInsatance } from '../../managers/DragDropTasksManager.js'; +import { ConfigureColumnSortingModal } from '../../modals/ConfigureColumnSortingModal.js'; +import { eventEmitter } from '../../services/EventEmitter.js'; +import { isRootFilterStateEmpty } from '../../utils/algorithms/AdvancedFilterer.js'; +import { matchTagsWithWildcards } from '../../utils/algorithms/ScanningFilterer.js'; +import { AdvancedFilterModal } from '../AdvancedFilterer/index.js'; +import { AdvancedFilterPopover } from '../AdvancedFilterer/Popover.js'; +import TaskItem, { swimlaneDataProp } from '../TaskCard/TaskItem.js'; +import TaskItemV2 from '../TaskCard/TaskItemV2.js'; type CustomCSSProperties = CSSProperties & { '--task-board-column-width': string; @@ -28,11 +26,13 @@ type CustomCSSProperties = CSSProperties & { export interface LazyColumnProps { plugin: TaskBoard; - columnIndex: number; activeBoardData: Board; - collapsed?: boolean; + currentViewIndex: number; + kanbanViewData: KanbanView; columnData: ColumnData; + // columnIndex: number; tasksForThisColumn: taskItem[]; + // collapsed?: boolean; swimlaneData?: swimlaneDataProp; hideColumnHeader?: boolean; headerOnly?: boolean; @@ -40,35 +40,40 @@ export interface LazyColumnProps { const LazyColumn: React.FC = ({ plugin, - columnIndex, activeBoardData, + currentViewIndex, + kanbanViewData, columnData, tasksForThisColumn, swimlaneData, hideColumnHeader = false, headerOnly = false, }) => { - if (!headerOnly && activeBoardData?.hideEmptyColumns && (tasksForThisColumn === undefined || tasksForThisColumn?.length === 0)) { - return null; // Don't render the column if it has no tasks and empty columns are hidden - } + // console.log("Column Data :", columnData); - // Lazy loading settings from plugin - const lazySettings = plugin.settings.data.globalSettings.kanbanView; - const initialTaskCount = lazySettings.initialTaskCount || 20; - const loadMoreCount = lazySettings.loadMoreCount || 10; - const scrollThresholdPercent = lazySettings.scrollThresholdPercent || 80; + // Lazy loading configs + const initialTaskCount = 20; + const loadMoreCount = 10; + const scrollThresholdPercent = 80; + const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); // State for managing visible tasks const [visibleTaskCount, setVisibleTaskCount] = useState(initialTaskCount); const tasksContainerRef = useRef(null); // Drag and drop state - const [isDragOver, setIsDragOver] = useState(false); + // const [isDragOver, setIsDragOver] = useState(false); const [insertIndex, setInsertIndex] = useState(null); const insertIndexRef = useRef(null); const rafRef = useRef(null); const [localTasks, setLocalTasks] = useState(tasksForThisColumn); + // Navigation visibility state + const prevScrollTopRef = useRef(0); + // const isNavHiddenRef = useRef(false); + const scrollPositionWhenHiddenRef = useRef(0); + const SCROLL_UP_THRESHOLD = 10; + const scheduleSetInsertIndex = (pos: number | null) => { if (insertIndexRef.current === pos) return; if (rafRef.current) { @@ -101,6 +106,37 @@ const LazyColumn: React.FC = ({ setLocalTasks(tasksForThisColumn); }, [tasksForThisColumn]); + const handleNavVisibility = () => { + const container = tasksContainerRef.current; + if (!container) return; + + const currentScrollTop = container.scrollTop; + const isScrollingDown = currentScrollTop > prevScrollTopRef.current; + const scrollDifference = Math.abs(currentScrollTop - prevScrollTopRef.current); + // console.log("LazyColumn.tsx...\ncurrentScrollTop:", currentScrollTop, "\nisScrollingDown :", isScrollingDown, "\nscrollDifference :", scrollDifference, "\nisNavHiddenRef :"); + + if (scrollDifference < 1) return; + + const htmlElement = document.documentElement; + + if (isScrollingDown) { + // User is scrolling down - hide navigation + htmlElement.classList.add('is-hidden-nav'); + // isNavHiddenRef.current = true; + scrollPositionWhenHiddenRef.current = currentScrollTop; + } else if (!isScrollingDown) { + // User is scrolling up - show navigation after scrolling up by threshold + const scrolledUpDistance = scrollPositionWhenHiddenRef.current - currentScrollTop; + if (scrolledUpDistance >= SCROLL_UP_THRESHOLD) { + htmlElement.classList.remove('is-hidden-nav'); + // isNavHiddenRef.current = false; + } + } + + // Update previous scroll position for next iteration + prevScrollTopRef.current = currentScrollTop; + }; + // Scroll event handler const handleScroll = useCallback(() => { const container = tasksContainerRef.current; @@ -129,6 +165,10 @@ const LazyColumn: React.FC = ({ if (throttleTimeout) return; throttleTimeout = setTimeout(() => { handleScroll(); + + if (Platform.isMobile) + handleNavVisibility(); + throttleTimeout = null; }, 100); }; @@ -140,10 +180,188 @@ const LazyColumn: React.FC = ({ }; }, [handleScroll]); - const columnWidth = plugin.settings.data.globalSettings.columnWidth || '273px'; + // ------------------------------------------------- + // ALL DRAG AND DROP RELATED FUNCTIONS + // + // All these drag-drop handlers has been moved at the top of this file + // so that useCallback can be initiazlied BEFORE any early returns + // ------------------------------------------------- + + /** + * This function will be only run when user will drag the taskItem on another taskItem. + * Computes insertion index based on mouse Y relative to task items inside the container. + */ + const handleTaskItemDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + // setIsDragOver(true); + try { + // Only compute insertion index for columns that use "manualOrder" as the sorting criteria. + if (!hasManualOrder) { + if (insertIndexRef.current !== null) { + scheduleSetInsertIndex(null); + } + dragDropTasksManagerInsatance.clearDesiredDropIndex(); + return; + } else { + // APPROACH 1 - COMPUTE INSERTION INDEX BASED ON MOUSE Y POSITION BY COMPARING WITH TASK ITEM BOUNDING RECTANGLES + // Else will proceed with finding the insertion index + // const container = e.currentTarget.parentElement as HTMLDivElement; + // const children = Array.from(container.querySelectorAll('.taskItemFadeIn')) as HTMLElement[]; + // let pos = children.length; // default to end + // const clientY = e.clientY; + // for (let i = 0; i < children.length; i++) { + // const child = children[i]; + // const rect = child.getBoundingClientRect(); + // const midpoint = rect.top + rect.height / 2; + // if (clientY < midpoint) { + // pos = i; + // break; + // } + // } + + // APPROACH 2 - DIRECTLY FETCH THE INDEX FROM THE DATA ATTRIBUTE OF THE HOVERED ELEMENT + let pos = 0; + const hoveredElement = e.currentTarget; + const draggedOverItemIndex = hoveredElement.getAttribute('data-taskitem-index'); + const draggedOverItemKey = hoveredElement.getAttribute('data-taskitem-id'); + const draggedItemKey = dragDropTasksManagerInsatance.getCurrentDragData()?.task.id; + + if (draggedOverItemKey && draggedOverItemIndex && draggedOverItemKey !== draggedItemKey) { + const clientY = e.clientY; + const rect = hoveredElement.getBoundingClientRect(); + const midpoint = rect.top + rect.height / 2; + if (clientY < midpoint) { + pos = parseInt(draggedOverItemIndex, 10); + } else { + pos = parseInt(draggedOverItemIndex, 10) + 1; + } + + // Throttle updates via RAF + scheduleSetInsertIndex(pos); + dragDropTasksManagerInsatance.setDesiredDropIndex(pos); + } else { + // Clear any visual placeholder and desired index + if (insertIndexRef.current !== null) { + scheduleSetInsertIndex(null); + } + dragDropTasksManagerInsatance.clearDesiredDropIndex(); + } + + const targetColumnContainer = tasksContainerRef.current as HTMLDivElement; + dragDropTasksManagerInsatance.handleCardDragOverEvent(e.nativeEvent as DragEvent, e.currentTarget as HTMLDivElement, targetColumnContainer, columnData); + } + } catch (error) { + bugReporterManagerInsatance.addToLogs(119, String(error), "LazyColumn.tsx/handleTaskItemDragOver"); + } + }, [scheduleSetInsertIndex, columnData]); + + /** + * This function will only run when user will drag the taskItem on a emtpy tasksContainer. + * If dragged over another taskItem within this tasksContainer, then handleTaskItemDragOver function will run. + */ + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + // setIsDragOver(true); + try { + // Get the target column container + const targetColumnContainer = (e.currentTarget) as HTMLDivElement; + dragDropTasksManagerInsatance.handleColumnDragOverEvent(e.nativeEvent, columnData, targetColumnContainer); + } catch (error) { + bugReporterManagerInsatance.addToLogs(120, String(error), "LazyColumn.tsx/handleDragOver"); + } + }, [columnData]); + + /** + * Handles the dragleave event to remove the visual effect + */ + const handleDragLeave = useCallback((e: React.DragEvent) => { + try { + // Avoid flicker: if the drag event indicates the pointer is still within the container bounds, + // ignore this dragleave (this happens when moving between child elements). + const container = e.currentTarget as HTMLElement; + const x = e.clientX; + const y = e.clientY; + if (typeof x === 'number' && typeof y === 'number') { + const rect = container.getBoundingClientRect(); + if (x >= rect.left + 10 && x <= rect.right - 10 && y >= rect.top && y <= rect.bottom) { + // still inside container — ignore to prevent CSS flicker + return; + } + } + } catch (err) { + console.log("While drag leave : ", err); + } + + // setIsDragOver(false); + setInsertIndex(null); + // Let manager clean up the column highlight + dragDropTasksManagerInsatance.handleDragLeaveEvent(e.currentTarget as HTMLDivElement); + dragDropTasksManagerInsatance.clearDesiredDropIndex(); + }, []); + + /** + * Handles the drop event of a task when its dropped over either another task or within an empty column. + * + * Moves the task from its orginal column to the target column. + * If manualSorting is enabled for the target column, Moves the task from its original + * position (dragIndex) to the new position (dropIndex). And updates the localTasks state + * and the columnData.tasksIdManualOrder. + * Clears the raf timer to prevent any pending raf calls. + * + * @param {React.DragEvent} e - The drag event. + */ + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + // setIsDragOver(false); + setInsertIndex(null); + + try { + // Get the target column container + const targetColumnContainer = tasksContainerRef.current; + // const targetColumnContainer = (e.currentTarget) as HTMLDivElement; + if (!targetColumnContainer) { + throw `e.currentTarget not found : ${JSON.stringify(targetColumnContainer)}`; + } + + dragDropTasksManagerInsatance.handleDropEvent(columnData, targetColumnContainer, swimlaneData); + dragDropTasksManagerInsatance.clearCurrentDragData(); + dragDropTasksManagerInsatance.clearDesiredDropIndex(); + + // clear any pending raf + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + } catch (error) { + bugReporterManagerInsatance.addToLogs(118, String(error), "LazyColumn.tsx/handleDrop"); + } + }, [columnData, plugin, swimlaneData]); + + // Cleanup any pending RAF on unmount + useEffect(() => { + return () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + // Clean up navigation visibility class when component unmounts + // if (isNavHiddenRef.current) { + document.documentElement.classList.remove('is-hidden-nav'); + // isNavHiddenRef.current = false; + // } + }; + }, []); + + // const shouldRenderEmptyColumn = !headerOnly && tasksForThisColumn?.length === 0; + // if (shouldRenderEmptyColumn) { + // return null; + // } + + const columnWidth = plugin.settings.data.columnWidth || '273px'; // Extra code to provide special data-types for theme support. - const tagColors = plugin.settings.data.globalSettings.tagColors; + const tagColors = plugin.settings.data.tagColors; const tagColorMap = new Map(tagColors.map((t) => [t.name, t])); let tagData = tagColorMap.get(columnData?.coltag || ''); if (!tagData) { @@ -156,52 +374,27 @@ const LazyColumn: React.FC = ({ // Determine whether an advanced filter is applied (used by header count UI) const isAdvancedFilterApplied = !isRootFilterStateEmpty(columnData.filters); - // If this column is requested to render header-only (used by swimlane top header), return just the header UI - if (headerOnly) { - return ( -
- {columnData.minimized ? ( -
openColumnMenu(evt)} aria-label={t("open-column-menu")}>{allTasks?.length ?? 0}
- ) : ( -
-
-
{columnData.name}
-
-
openColumnMenu(evt)} aria-label={t("open-column-menu")}> - {allTasks?.length ?? 0} -
-
- )} -
- ); - } - async function handleMinimizeColumn() { // const boardIndex = plugin.settings.data.boardConfigs.findIndex( // (board: Board) => board.name === activeBoardData.name // ); - const boardIndex = activeBoardData.index; - - if (boardIndex !== -1) { - // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. - // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - // (col: ColumnData) => col.id === columnData.id - // ); - const columnIndex = columnData.index - 1; - - if (columnIndex !== -1) { - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].minimized = !plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].minimized; - await plugin.saveSettings(); - eventEmitter.emit('REFRESH_BOARD'); - } + + // const boardIndex = activeBoardData.index; + // if (boardIndex !== -1) { + // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. + // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( + // (col: ColumnData) => col.id === columnData.id + // ); + const columnIndex = columnData.index - 1; + + if (columnIndex !== -1) { + let newBoardData = activeBoardData; + newBoardData.views[currentViewIndex].kanbanView!.columns[columnIndex].minimized = !newBoardData.views[currentViewIndex].kanbanView!.columns[columnIndex].minimized; + plugin.taskBoardFileManager.saveBoard(newBoardData); + + eventEmitter.emit('REFRESH_BOARD'); } + // } } async function handleAlertButtonClick() { @@ -232,25 +425,22 @@ const LazyColumn: React.FC = ({ columnData, (updatedColumnConfiguration: ColumnData) => { // Update the column configuration in the board data - const boardIndex = plugin.settings.data.boardConfigs.findIndex( - (board: Board) => board.index === activeBoardData.index - ); - if (boardIndex !== -1) { - const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - (col: ColumnData) => col.id === columnData.id - ); - - if (columnIndex !== -1) { - // Update the column configuration - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex] = updatedColumnConfiguration; + // if (activeBoardData.index !== -1) { + const columnIndex = activeBoardData.views[currentViewIndex].kanbanView!.columns.findIndex( + (col: ColumnData) => col.id === columnData.id + ); - // Save the settings - plugin.saveSettings(); + if (columnIndex !== -1) { + // Update the column configuration + let newBoardData = activeBoardData; + newBoardData.views[currentViewIndex].kanbanView!.columns[columnIndex] = updatedColumnConfiguration; + plugin.taskBoardFileManager.saveBoard(newBoardData); - eventEmitter.emit('REFRESH_BOARD'); - } + // Refresh the board view + eventEmitter.emit('REFRESH_BOARD'); } + // } }, () => { // onCancel callback - nothing to do @@ -267,28 +457,28 @@ const LazyColumn: React.FC = ({ // const boardIndex = plugin.settings.data.boardConfigs.findIndex( // (board: Board) => board.name === activeBoardData.name // ); - const boardIndex = activeBoardData.index; + // const boardIndex = activeBoardData.index; // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( // (col: ColumnData) => col.id === columnData.id // ); - const columnIndex = columnData.index - 1; + const columnIndex = columnData.index - 1; if (Platform.isMobile || Platform.isMacOS) { // If its a mobile platform, then we will open a modal instead of popover. - const filterModal = new ViewTaskFilterModal( - plugin, true, undefined, boardIndex, columnData.name, columnData.filters + const filterModal = new AdvancedFilterModal( + plugin, true, activeBoardData.id, columnData.name, columnData.filters ); // Set the close callback - mainly used for handling cancel actions filterModal.filterCloseCallback = async (filterState) => { - if (filterState && boardIndex !== -1) { + if (filterState) { if (columnIndex !== -1) { // Update the column filters - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].filters = filterState; + let newBoardData = activeBoardData; + newBoardData.views[currentViewIndex].kanbanView!.columns[columnIndex].filters = filterState; - // Save the settings - await plugin.saveSettings(); + plugin.taskBoardFileManager.saveBoard(newBoardData); // Refresh the board view eventEmitter.emit('REFRESH_BOARD'); @@ -308,24 +498,23 @@ const LazyColumn: React.FC = ({ // Create and show filter popover // leafId is undefined for column filters (not tied to a specific leaf) - const popover = new ViewTaskFilterPopover( + const popover = new AdvancedFilterPopover( plugin, true, // forColumn is true - undefined, - boardIndex, + activeBoardData.id, columnData.name, columnData.filters ); // Set up close callback to save filter state popover.onClose = async (filterState?: RootFilterState) => { - if (filterState && boardIndex !== -1) { + if (filterState) { if (columnIndex !== -1) { // Update the column filters - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].filters = filterState; + let newBoardData = activeBoardData; + newBoardData.views[currentViewIndex].kanbanView!.columns[columnIndex].filters = filterState; - // Save the settings - await plugin.saveSettings(); + plugin.taskBoardFileManager.saveBoard(newBoardData); // Refresh the board view eventEmitter.emit('REFRESH_BOARD'); @@ -354,26 +543,26 @@ const LazyColumn: React.FC = ({ // const boardIndex = plugin.settings.data.boardConfigs.findIndex( // (board: Board) => board.name === activeBoardData.name // ); - const boardIndex = activeBoardData.index; + // const boardIndex = activeBoardData.index; - if (boardIndex !== -1) { - // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. - // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( - // (col: ColumnData) => col.id === columnData.id - // ); - const columnIndex = columnData.index - 1; + // if (boardIndex !== -1) { + // NOTE : This extra thing we need to do because, the columnData.index is stored starting with 1 and not 0. Hence, I we will need to subtract 1 from it. + // const columnIndex = plugin.settings.data.boardConfigs[boardIndex].columns.findIndex( + // (col: ColumnData) => col.id === columnData.id + // ); + const columnIndex = columnData.index - 1; - if (columnIndex !== -1) { - // Set the active property to false - plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].active = false; + if (columnIndex !== -1) { + // Set the active property to false + let newBoardData = activeBoardData; + newBoardData.views[currentViewIndex].kanbanView!.columns[columnIndex].active = false; - // Save the settings - await plugin.saveSettings(); + plugin.taskBoardFileManager.saveBoard(newBoardData); - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } + // Refresh the board view + eventEmitter.emit('REFRESH_BOARD'); } + // } }); }); @@ -396,6 +585,39 @@ const LazyColumn: React.FC = ({ }); } + // Show swimlane toggle option only when swimlanes are enabled + const isSwimlanesEnabled = kanbanViewData.swimlanes.enabled; + if (isSwimlanesEnabled) { + columnMenu.addSeparator(); + columnMenu.addItem((item) => { + const isSwimlaneEnabled = columnData.swimlaneEnabled; + item.setTitle(isSwimlaneEnabled ? t("exclude-from-swimlanes") : t("include-in-swimlanes")); + item.setIcon(isSwimlaneEnabled ? "layout-panel-left" : "rows-3"); + item.onClick(async () => { + let updatedViewData = { ...kanbanViewData }; + updatedViewData.columns[columnData.index - 1].swimlaneEnabled = !isSwimlaneEnabled; + + let updatedBoardData = { ...activeBoardData }; + if (updatedBoardData.views[currentViewIndex].kanbanView) { + updatedBoardData.views[currentViewIndex].kanbanView = updatedViewData; + plugin.taskBoardFileManager.saveBoard(updatedBoardData); + + eventEmitter.emit('REFRESH_BOARD'); + } + + // const boardIndex = activeBoardData.index; + // if (boardIndex !== -1) { + // const columnIndex = columnData.index - 1; + // if (columnIndex !== -1) { + // plugin.settings.data.boardConfigs[boardIndex].columns[columnIndex].swimlaneEnabled = !isSwimlaneEnabled; + // await plugin.saveSettings(); + // eventEmitter.emit('REFRESH_BOARD'); + // } + // } + }); + }); + } + // Use native event if available (React event has nativeEvent property) columnMenu.showAtMouseEvent( (event instanceof MouseEvent ? event : event.nativeEvent) @@ -403,471 +625,163 @@ const LazyColumn: React.FC = ({ } // ------------------------------------------------- - // ALL DRAG AND DROP RELATED FUNCTIONS + // ALL DRAG AND DROP RELATED FUNCTIONS ARE DEFINED ABOVE TO PREVENT HOOK ORDERING ISSUES // ------------------------------------------------- - /** - * Handles the drop event of a task in this column. - * Moves the task from its original position (dragIndex) to the new position (dropIndex). - * Updates the localTasks state and the columnData.tasksIdManualOrder if the column uses manualOrder. - * Clears the raf timer to prevent any pending raf calls. - * @param {React.DragEvent} e - The drag event. - * @param {number} dropIndex - The index at which to drop the task. - */ - const handleTaskDrop = async (e: React.DragEvent, dropIndex: number) => { - e.preventDefault(); - setIsDragOver(false); - setInsertIndex(null); - - const targetColumnContainer = tasksContainerRef.current; - if (!targetColumnContainer) { - return; - } - - // We are basically doing same thing from the handleDrop function below. - dragDropTasksManagerInsatance.handleDropEvent( - e.nativeEvent, - columnData, - targetColumnContainer, - swimlaneData - ); - - // Clear manager payload (drag finished) - dragDropTasksManagerInsatance.clearCurrentDragData(); - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - - // const dragIndex = parseInt(e.dataTransfer.getData('text/plain')); - // if (isNaN(dragIndex) || dragIndex === dropIndex) return; - // const updated = [...localTasks]; - // const [moved] = updated.splice(dragIndex, 1); - // updated.splice(dropIndex, 0, moved); - // setLocalTasks(updated); - // // If this column uses manualOrder, update the columnData.tasksIdManualOrder to reflect new order - // const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - // if (hasManualOrder) { - // columnData.tasksIdManualOrder = updated.map(t => t.id); - // } - - // clear any pending raf - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - rafRef.current = null; - } - }; - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - - try { - // Get the data of the dragged task -- No need anymore, since its already stored in the dragdropmanager. - // const taskData = e.dataTransfer.getData('application/json'); - // if (taskData) { - // const { task, sourceColumnData } = JSON.parse(taskData); - - // // Ensure we have valid data - // if (!task || !sourceColumnData) return; - - // Get the target column container - const targetColumnContainer = (e.currentTarget) as HTMLDivElement; - - - // Try to locate the source container by stable column id first (works for all colTypes) -- No need to find this anymore, since I am not making use of sourceColumnContainer in dragdropmanager. - // let sourceColumnContainer: HTMLDivElement | null = null; - // if (sourceColumnData?.id) { - // try { - // const escapedId = CSS.escape(String(sourceColumnData.id)); - // sourceColumnContainer = document.querySelector(`.TaskBoardColumnsSection[data-column-id="${escapedId}"]`) as HTMLDivElement | null; - // } catch (err) { - // // fallback to tag-based lookup below - // } - // } - // if (!sourceColumnContainer) { - // // Fallback: find by tag name (legacy behavior) - // console.log("------------- I hope this fall-back mechanism is never running -------------"); - // const allColumnContainers = Array.from(document.querySelectorAll('.TaskBoardColumnsSection')) as HTMLDivElement[]; - // sourceColumnContainer = allColumnContainers.find(container => { - // const containerTag = container.getAttribute('data-column-tag-name'); - // return containerTag === sourceColumnData.coltag || sourceColumnData.coltag?.includes(containerTag || ''); - // }) || targetColumnContainer; - // } - - // we will allow cross-column drops now with target column having manualOrder sortCriteria. Disabling below code. - // const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - // if (hasManualOrder && sourceColumnData.id !== columnData.id) { - // // Not allowed: ignore drop - // dragDropTasksManagerInsatance.clearCurrentDragData(); - // dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // return; - // } - - // // Use the DragDropTasksManager to handle the drop - // try { - // const dragIdxStr = e.dataTransfer.getData('text/plain'); - // const dragIdx = dragIdxStr ? parseInt(dragIdxStr) : NaN; - // if (sourceColumnData.coltag === columnData.coltag && !isNaN(dragIdx) && insertIndexRef.current !== null) { - // // Reorder locally - // const updated = [...localTasks]; - // const [moved] = updated.splice(dragIdx, 1); - // updated.splice(insertIndexRef.current!, 0, moved); - // setLocalTasks(updated); - // setInsertIndex(null); - // insertIndexRef.current = null; - // // Update manual order if applicable - // const hasManualOrderLocal = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - // if (hasManualOrderLocal) { - // columnData.tasksIdManualOrder = updated.map(t => t.id); - // } - // // Clear manager payload and skip default handling - // dragDropTasksManagerInsatance.clearCurrentDragData(); - // dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // return; - // } - // } catch (err) { - // // ignore and continue to default handling - // } - - dragDropTasksManagerInsatance.handleDropEvent( - e.nativeEvent, - columnData, - targetColumnContainer, - swimlaneData - ); - - // Clear manager payload (drag finished) - dragDropTasksManagerInsatance.clearCurrentDragData(); - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // } - } catch (error) { - console.error('Error handling task drop:', error); - } - }, [columnData, plugin]); - - // This function will be only run when user will drag the taskItem on another taskItem. - // Compute insertion index based on mouse Y relative to task items inside the container. - const handleTaskItemDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - try { - // Only compute insertion index for columns that use "manualOrder" as the sorting criteria. - const hasManualOrder = Array.isArray(columnData.sortCriteria) && columnData.sortCriteria.some((c) => c.criteria === 'manualOrder'); - if (!hasManualOrder) { - // Clear any visual placeholder and desired index - if (insertIndexRef.current !== null) { - scheduleSetInsertIndex(null); - } - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - return; - } else { - // APPROACH 1 - COMPUTE INSERTION INDEX BASED ON MOUSE Y POSITION BY COMPARING WITH TASK ITEM BOUNDING RECTANGLES - // Else will proceed with finding the insertion index - // const container = e.currentTarget.parentElement as HTMLDivElement; - // const children = Array.from(container.querySelectorAll('.taskItemFadeIn')) as HTMLElement[]; - // let pos = children.length; // default to end - // const clientY = e.clientY; - // for (let i = 0; i < children.length; i++) { - // const child = children[i]; - // const rect = child.getBoundingClientRect(); - // const midpoint = rect.top + rect.height / 2; - // if (clientY < midpoint) { - // pos = i; - // break; - // } - // } - - // APPROACH 2 - DIRECTLY FETCH THE INDEX FROM THE DATA ATTRIBUTE OF THE HOVERED ELEMENT - let pos = 0 // Default to top of the column - const hoveredElement = e.currentTarget; - const draggedOverItemIndex = hoveredElement.getAttribute('data-taskitem-index'); - const draggedOverItemKey = hoveredElement.getAttribute('data-taskitem-id'); - const draggedItemKey = dragDropTasksManagerInsatance.getCurrentDragData()?.task.id; - // console.log('handleTaskItemDragOver... \ndataAttribute', draggedOverItemIndex, "\ndraggedItemIndex", draggedItemIndex); - if (draggedOverItemKey && draggedOverItemIndex && draggedOverItemKey !== draggedItemKey) { - const clientY = e.clientY; - const rect = hoveredElement.getBoundingClientRect(); - const midpoint = rect.top + rect.height / 2; - if (clientY < midpoint) { - pos = parseInt(draggedOverItemIndex, 10); - } else { - pos = parseInt(draggedOverItemIndex, 10) + 1; - } - - // Throttle updates via RAF - scheduleSetInsertIndex(pos); - // Store desired drop index in manager - dragDropTasksManagerInsatance.setDesiredDropIndex(pos); - } else { - // Clear any visual placeholder and desired index - if (insertIndexRef.current !== null) { - scheduleSetInsertIndex(null); - } - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - } - - - // // Use the DragDropTasksManager to handle the drag over (this sets classes and dropEffect) - // dragDropTasksManagerInsatance.handleDragOver( - // e.nativeEvent, - // columnData, - // container - // ); - - const targetColumnContainer = tasksContainerRef.current as HTMLDivElement; - dragDropTasksManagerInsatance.handleCardDragOverEvent(e.nativeEvent as DragEvent, e.currentTarget as HTMLDivElement, targetColumnContainer, columnData); - } - } catch (error) { - console.error('Error computing insert index:', error); - } - }, [scheduleSetInsertIndex, columnData]); - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - try { - // // Try to read payload from the DataTransfer first - // let taskDataStr = ''; - // try { - // taskDataStr = e.dataTransfer.getData('application/json'); - // } catch (err) { - // // ignore - some environments restrict access - // } - - // let payload: any = null; - // if (taskDataStr) { - // try { payload = JSON.parse(taskDataStr); } catch { } - // } - - // // Fallback to manager-stored payload if dataTransfer is empty - // if (!payload) { - // payload = dragDropTasksManagerInsatance.getCurrentDragData(); - // } - - // if (!payload) return; - - // const { task, sourceColumnData } = payload; - // if (!task || !sourceColumnData) return; - - // Get the target column container - const targetColumnContainer = (e.currentTarget) as HTMLDivElement; - - // // Try id-based lookup first - // let sourceColumnContainer: HTMLDivElement | null = null; - // if (sourceColumnData?.id) { - // try { - // const escapedId = CSS.escape(String(sourceColumnData.id)); - // sourceColumnContainer = document.querySelector(`.TaskBoardColumnsSection[data-column-id="${escapedId}"]`) as HTMLDivElement | null; - // } catch (err) { - // // ignore and fall back to tag-based lookup - // } - // } - // if (!sourceColumnContainer) { - // const allColumnContainers = Array.from(document.querySelectorAll('.TaskBoardColumnsSection')) as HTMLDivElement[]; - // sourceColumnContainer = allColumnContainers.find(container => { - // const containerTag = container.getAttribute('data-column-tag-name'); - // return containerTag === sourceColumnData.coltag || sourceColumnData.coltag?.includes(containerTag || ''); - // }) || targetColumnContainer; - // } - - // Use the DragDropTasksManager to handle the drag over (this sets classes and dropEffect) - dragDropTasksManagerInsatance.handleColumnDragOverEvent( - e.nativeEvent, - columnData, - targetColumnContainer - ); - - // Below code is not required, since, I will call the dragDropTasksManagerInsatance.handleCardDragOverEvent from handleTaskItemDragOver. - // // If hovering over an actual card element, show card drop indicator - // try { - // const hovered = (e.target as HTMLElement).closest('.taskItem') as HTMLElement | null; - // if (hovered) { - // dragDropTasksManagerInsatance.handleCardDragOverEvent(e.nativeEvent as DragEvent, hovered); - // } - // } catch (err) { - // // ignore - // } - - // // Ensure cursor reflects allowed/not-allowed (best-effort fallback) - // const allowed = dragDropTasksManagerInsatance.isTaskDropAllowed(sourceColumnData, columnData); - // e.dataTransfer!.dropEffect = allowed ? 'move' : 'none'; - } catch (error) { - console.error('Error handling drag over:', error); - } - }, [columnData]); - - // Cleanup any pending RAF on unmount - useEffect(() => { - return () => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current); - rafRef.current = null; - } - }; - }, []); - - // Handle the dragleave event to remove the visual effect - const handleDragLeave = useCallback((e: React.DragEvent) => { - // Avoid flicker: if the drag event indicates the pointer is still within the container bounds, - // ignore this dragleave (this happens when moving between child elements). - try { - const container = e.currentTarget as HTMLElement; - const x = e.clientX; - const y = e.clientY; - if (typeof x === 'number' && typeof y === 'number') { - const rect = container.getBoundingClientRect(); - if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { - // still inside container — ignore to prevent CSS flicker - return; - } - } - } catch (err) { - // ignore and continue cleanup - } - - setIsDragOver(false); - setInsertIndex(null); - dragDropTasksManagerInsatance.clearDesiredDropIndex(); - // Let manager clean up the dropindicator and column highlight - dragDropTasksManagerInsatance.handleDragLeaveEvent(e.currentTarget as HTMLDivElement); - }, []); - - // ------------------------------------------------- // Render // ------------------------------------------------- - const taskItemComponent = plugin.settings.data.globalSettings.taskCardStyle === taskCardStyleNames.EMOJI ? TaskItem : TaskItemV2; - - return ( -
- {columnData.minimized && !hideColumnHeader ? ( - // Minimized view -
-
openColumnMenu(evt)} aria-label={t("open-column-menu")}> - {allTasks?.length ?? 0} + // If this column is requested to render header-only (used by swimlane top header), return just the header UI + if (headerOnly) { + return ( +
+ {columnData.minimized ? ( +
openColumnMenu(evt)} aria-label={t("open-column-menu")}>{allTasks?.length ?? 0}
+ ) : ( +
+
+
{columnData.name}
+
+
openColumnMenu(evt)} aria-label={t("open-column-menu")}> + {allTasks?.length ?? 0} +
-
{ - await handleMinimizeColumn(); - eventEmitter.emit('REFRESH_BOARD'); - }}>{columnData.name}
-
- ) : ( - // Normal view - <> - {!hideColumnHeader && ( -
-
-
{columnData.name}
- {columnData?.workLimit && tasksForThisColumn.length > columnData.workLimit && ( -
- -
- )} -
-
openColumnMenu(evt)} aria-label={t("open-column-menu")}> - {allTasks?.length ?? 0} -
+ )} +
+ ); + } + + const taskItemComponent = plugin.settings.data.taskCardStyle === taskCardStyleNames.EMOJI ? TaskItem : TaskItemV2; + + try { + return ( +
+ {columnData.minimized && !hideColumnHeader ? ( + // Minimized view +
+
openColumnMenu(evt)} aria-label={t("open-column-menu")}> + {allTasks?.length ?? 0}
- )} -
{ handleDragOver(e); }} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - onDragEnd={(e) => { setIsDragOver(false); setInsertIndex(null); dragDropTasksManagerInsatance.clearAllDragStyling(); }} - > - {columnData.minimized ? <> : ( - <> - {visibleTasks && visibleTasks.length > 0 ? ( - <> - {(() => { - const elements: React.ReactNode[] = []; - for (let i = 0; i < visibleTasks.length; i++) { - // If insertIndex points to this position, render placeholder - if (insertIndex === i) { +
{ + await handleMinimizeColumn(); + eventEmitter.emit('REFRESH_BOARD'); + }}>{columnData.name}
+
+ ) : ( + // Normal view + <> + {!hideColumnHeader && ( +
+
+
{columnData.name}
+ {columnData?.workLimit && tasksForThisColumn.length > columnData.workLimit && ( +
+ +
+ )} +
+
openColumnMenu(evt)} aria-label={t("open-column-menu")}> + {allTasks?.length ?? 0} +
+
+ )} +
{ handleDragOver(e); }} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onDragEnd={(e) => { + // setIsDragOver(false); + setInsertIndex(null); + dragDropTasksManagerInsatance.clearAllDragStyling(); + }} + > + {columnData.minimized ? <> : ( + <> + {visibleTasks && visibleTasks.length > 0 ? ( + <> + {(() => { + const elements: React.ReactNode[] = []; + for (let i = 0; i < visibleTasks.length; i++) { + // If insertIndex points to this position, render placeholder + if (insertIndex === i) { + elements.push( +
Drop here
+ ); + } + const task = visibleTasks[i]; elements.push( -
Drop here
+
{ handleTaskItemDragOver(e); } + } + onDrop={e => handleDrop(e)} + > + {React.createElement(taskItemComponent, { + key: task.id, + dataAttributeIndex: i, + plugin: plugin, + task: task, + activeBoardID: activeBoardData.id, + activeViewIndex: currentViewIndex, + activeViewType: viewTypeNames.kanban, // Since LazyColumn will be always rendered inside a Kanban view. + kanbanViewData: kanbanViewData, + columnIndex: columnData.index, + swimlaneData: swimlaneData + })} +
); } - const task = visibleTasks[i]; - elements.push( -
{ handleTaskItemDragOver(e); } - } - onDrop={e => handleTaskDrop(e, i)} - > - -
- ); - } - // If insertIndex points to end (after last item) - if (insertIndex === localTasks.length) { - elements.push( -
Drop here
- ); - } - return elements; - })()} - {allTasks && visibleTaskCount < allTasks.length && ( -
-

{t("scroll-to-load-more")} ({visibleTaskCount} / {allTasks.length ?? 0})

-
- )} - - ) : ( -
{ e.preventDefault(); }}> -

{t("no-tasks-available")}

-
- )} - - ) - } -
- - ) - } -
- ); + // If insertIndex points to end (after last item) + if (localTasks && insertIndex === localTasks.length) { + elements.push( +
Drop here
+ ); + } + return elements; + })()} + {allTasks && visibleTaskCount < allTasks.length && ( +
+

{t("scroll-to-load-more")} ({visibleTaskCount} / {allTasks.length ?? 0})

+
+ )} + + ) : ( +
{ e.preventDefault(); }}> +

{t("no-tasks-available")}

+
+ )} + + ) + } +
+ + ) + } +
+ ); + } catch (error) { + bugReporterManagerInsatance.showNotice(180, "There was an issue rendering a particular column. This might cause the whole tab to go blank. Try, closing and opening Task Board again. If the issue still persists, please report this to the developer", JSON.stringify(error), "LazyColumn.tsx/return"); + } }; -const MemoizedTaskItem = memo<{ - Component: typeof TaskItem | typeof TaskItemV2; - dataAttributeIndex: number; - plugin: TaskBoard; - task: taskItem; - activeBoardSettings: Board; - columnIndex?: number; - swimlaneData?: swimlaneDataProp; -}>(({ Component, ...props }) => { - return ; -}, (prevProps, nextProps) => { - return ( - prevProps.dataAttributeIndex === nextProps.dataAttributeIndex && - prevProps.task === nextProps.task && - prevProps.activeBoardSettings === nextProps.activeBoardSettings && - prevProps.columnIndex === nextProps.columnIndex && - prevProps.swimlaneData === nextProps.swimlaneData - ); -}); - export default memo(LazyColumn); diff --git a/src/components/MapView/CustomNodeResizer.tsx b/src/components/MapView/CustomNodeResizer.tsx index 16c09f68..f655c8da 100644 --- a/src/components/MapView/CustomNodeResizer.tsx +++ b/src/components/MapView/CustomNodeResizer.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import { Handle, Position, NodeResizeControl, NodeProps } from '@xyflow/react'; -import type TaskBoard from 'main'; +import TaskBoard from '../../../main.js'; interface dataProps extends React.ReactElement { props: { plugin: TaskBoard }; diff --git a/src/components/MapView/EdgeWithToolbar.tsx b/src/components/MapView/EdgeWithToolbar.tsx index 07b04a9b..9f7a5cf9 100644 --- a/src/components/MapView/EdgeWithToolbar.tsx +++ b/src/components/MapView/EdgeWithToolbar.tsx @@ -9,10 +9,6 @@ import { useReactFlow, } from '@xyflow/react'; import { Trash2, Palette, Sparkles } from 'lucide-react'; -import TaskBoard from 'main'; -import { taskItem } from 'src/interfaces/TaskItem'; -import { updateTaskInFile } from 'src/utils/taskLine/TaskLineUtils'; -import { sanitizeDependsOn } from 'src/utils/taskLine/TaskContentFormatter'; // interface EdgeWithToolbarProps extends EdgeProps { // plugin: TaskBoard; @@ -58,7 +54,11 @@ export function EdgeWithToolbar(props: EdgeProps) { // ); // if (!targetTask) { - // console.warn('Target task not found for edge deletion'); + // bugReporterManagerInsatance.addToLogs( + // 171, + // `Target task not found for edge deletion`, + // "EdgeWithToolbar.tsx/deleteEdge", + // ); // return; // } @@ -72,7 +72,7 @@ export function EdgeWithToolbar(props: EdgeProps) { // // Update the task title to reflect the removed dependency // const updatedTaskTitle = sanitizeDependsOn( - // plugin.settings.data.globalSettings, + // plugin.settings.data, // updatedTargetTask.title, // updatedTargetTask.dependsOn // ); diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index bc73a5ad..da06f4be 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -1,6 +1,6 @@ // /src/components/MapView/MapView.tsx -import React, { useState, useEffect, useRef, memo, useMemo } from 'react'; +import React, { useState, useEffect, useRef, memo, useMemo, useCallback } from 'react'; import { ReactFlow, ReactFlowProvider, @@ -16,50 +16,33 @@ import { ControlButton, } from '@xyflow/react'; // import '@xyflow/react/dist/style.css'; -import { taskItem, UpdateTaskEventData } from 'src/interfaces/TaskItem'; -import TaskBoard from 'main'; -import ResizableNodeSelected from './ResizableNodeSelected'; -import TaskItem from '../KanbanView/TaskItem'; -import { updateTaskInFile } from 'src/utils/taskLine/TaskLineUtils'; import { debounce, Menu, Notice, Platform } from 'obsidian'; -import { NODE_POSITIONS_STORAGE_KEY, NODE_SIZE_STORAGE_KEY, VIEWPORT_STORAGE_KEY } from 'src/interfaces/Constants'; -import { sanitizeDependsOn } from 'src/utils/taskLine/TaskContentFormatter'; -import { t } from 'src/utils/lang/helper'; -import { MapViewMinimap } from './MapViewMinimap'; -import { mapViewArrowDirection, mapViewBackgrounVariantTypes, mapViewScrollAction } from 'src/interfaces/Enums'; -import { eventEmitter } from 'src/services/EventEmitter'; -import { bugReporter } from 'src/services/OpenModals'; import { PanelLeftOpenIcon } from 'lucide-react'; -import { TasksImporterPanel } from './TasksImporterPanel'; -import { isTaskNotePresentInTags, updateFrontmatterInMarkdownFile } from 'src/utils/taskNote/TaskNoteUtils'; -import { isTaskCompleted } from 'src/utils/CheckBoxUtils'; -import { bugReporterManagerInsatance } from 'src/managers/BugReporter'; +import { t } from 'i18next'; +import TaskBoard from '../../../main.js'; +import { Board, TaskBoardViewType, viewPortType, nodeDataType, nodePositionData } from '../../interfaces/BoardConfigs.js'; +import { mapViewBackgrounVariantTypes, viewTypeNames, mapViewScrollAction } from '../../interfaces/Enums.js'; +import { taskJsonMerged, taskItem, UpdateTaskEventData } from '../../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +import { eventEmitter } from '../../services/EventEmitter.js'; +import { isTaskCompleted } from '../../utils/CheckBoxUtils.js'; +import { sanitizeDependsOn } from '../../utils/taskLine/TaskContentFormatter.js'; +import { updateTaskInFile } from '../../utils/taskLine/TaskLineUtils.js'; +import { isTaskNotePresentInTags, updateFrontmatterInMarkdownFile } from '../../utils/taskNote/TaskNoteUtils.js'; +import TaskItem from '../TaskCard/TaskItem.js'; +import { MapViewMinimap } from './MapViewMinimap.js'; +import ResizableNodeSelected from './ResizableNodeSelected.js'; +import { TasksImporterPanel } from './TasksImporterPanel.js'; type MapViewProps = { plugin: TaskBoard; - activeBoardIndex: number; - allTasksArranged: taskItem[][]; - // loading: boolean; - // freshInstall: boolean; + activeBoardData: Board; + currentView: TaskBoardViewType; + currentViewIndex: number; + filteredTasks: taskJsonMerged; focusOnTaskId?: string; }; -export type viewPort = { - x: number; - y: number; - zoom: number; -} - -export type nodeSize = { - width: number; - // height: number; -} - -export type nodePosition = { - x: number; - y: number; -} - const nodeTypes = { // CustomNodeResizer, ResizableNodeSelected, @@ -67,12 +50,12 @@ const nodeTypes = { const MapView: React.FC = ({ - plugin, activeBoardIndex, allTasksArranged, focusOnTaskId + plugin, activeBoardData, currentView, currentViewIndex, filteredTasks, focusOnTaskId }) => { - plugin.settings.data.globalSettings.lastViewHistory.taskId = ""; // Clear the taskId after focusing once - const mapViewSettings = plugin.settings.data.globalSettings.mapView; - const taskNoteIdentifierTag = plugin.settings.data.globalSettings.taskNoteIdentifierTag; - + plugin.settings.data.lastViewHistory.taskId = ""; // Clear the taskId after focusing once + const mapViewSettings = plugin.settings.data.mapView; + const taskNoteIdentifierTag = plugin.settings.data.taskNoteIdentifierTag; + const tagColors = plugin.settings.data.tagColors; const userBackgroundVariant: BackgroundVariant | undefined = (() => { switch (mapViewSettings.background) { case mapViewBackgrounVariantTypes.dots: @@ -85,126 +68,160 @@ const MapView: React.FC = ({ return undefined; } })(); - const tagColors = plugin.settings.data.globalSettings.tagColors; - const activeBoardSettings = plugin.settings.data.boardConfigs[activeBoardIndex]; - // Loading state for localStorage data + // Flatten the filtered tasks from taskJsonMerged to a single array for MapView + // IMPORTANT: Memoize to prevent infinite loop - prevents recreating array on every render + const allTasksFlattened = useMemo(() => + filteredTasks ? [...filteredTasks.Completed, ...filteredTasks.Pending] : [], + [filteredTasks] + ); + // Loading state for board map data (stored on the board object) const [storageLoaded, setStorageLoaded] = useState(false); - const [positions, setPositions] = useState>({}); - const [nodeSizes, setNodeSizes] = useState>({}); - const [viewport, setViewport] = useState>({}); - - // Track when board changes to force node recalculation - const [boardChangeKey, setBoardChangeKey] = useState(0); - - // Task importer panel state - const [isImporterPanelVisible, setIsImporterPanelVisible] = useState(false); - + // const [activeBoardSettings, setActiveBoardSettings] = useState(activeBoardData) + const [boardChangeKey, setBoardChangeKey] = useState(0); // Track when board changes to force node recalculation + const [isImporterPanelVisible, setIsImporterPanelVisible] = useState(false); // Task importer panel state + + const mapDataUpdated = useRef(false); + const viewPortDataUpdated = useRef(false); + // Store node positions in ref to avoid re-renders during drag operations + const allNodesData = useRef({}); + const viewPortData = useRef({ x: 10, y: 10, zoom: 1 }); + // const [viewport, setViewport] = useState({ x: 10, y: 10, zoom: 1 }); // ReactFlow instance ref so we can programmatically set viewport when switching boards - const reactFlowInstanceRef = useRef(null); + // const reactFlowInstanceRef = useRef(null); - // Load positions from localStorage, board-wise - const loadPositions = () => { - let allBoardPositions: Record> = {}; - try { - const stored = localStorage.getItem(NODE_POSITIONS_STORAGE_KEY); - if (stored) { - allBoardPositions = JSON.parse(stored); - // Validate the structure - if (typeof allBoardPositions !== 'object' || allBoardPositions === null) { - allBoardPositions = {}; - } - } - } catch (error) { - console.warn('Failed to load node positions from localStorage:', error); - allBoardPositions = {}; - } + // ------------------------------------------------------------- + // LOADING MAP DATA FROM BOARD FILE + // ------------------------------------------------------------- - try { - const boardPositions = allBoardPositions[activeBoardIndex]; - if (typeof boardPositions === 'object' && boardPositions !== null) { - return boardPositions; - } - return {}; - } catch (error) { - console.warn('Failed to get positions for board', activeBoardIndex, ':', error); - return {}; - } - }; + // Load positions from the active board data + const loadAllNodesData = (): nodeDataType => { + console.log("loadAllNodesData called..."); - // Load node sizes from localStorage - const loadNodeSizes = () => { try { - const stored = localStorage.getItem(NODE_SIZE_STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - if (typeof parsed === 'object' && parsed !== null) { - return parsed as Record; - } - } - return {}; + const list = currentView?.mapView?.nodesData ? currentView.mapView.nodesData : {}; + // const map: Record = {}; + // list.forEach(item => { + // if (item && typeof item.key === 'string') { + // map[item.id] = { x: Number.isFinite(item.x) ? item.x : 0, y: Number.isFinite(item.y) ? item.y : 0 }; + // } + // }); + return list; } catch (error) { - console.warn('Failed to load node sizes from localStorage:', error); + bugReporterManagerInsatance.addToLogs(92, String(error), 'MapView.tsx/loadPositions'); return {}; } }; - // Viewport state (board-wise) - const loadViewport = (): Record => { + // Load viewport from the active board data + const loadViewport = (): viewPortType => { + console.log("loadViewport called..."); try { - const stored = localStorage.getItem(VIEWPORT_STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - if (typeof parsed === 'object' && parsed !== null) { - return parsed as Record; - } + const vp = currentView?.mapView?.viewPortData; + if (vp && typeof vp === 'object') { + return { + x: Number.isFinite(vp.x) ? vp.x : 10, + y: Number.isFinite(vp.y) ? vp.y : 10, + zoom: Number.isFinite(vp.zoom) && vp.zoom > 0 ? vp.zoom : 1, + }; } - return { [activeBoardIndex]: { x: 10, y: 10, zoom: 1.5 } }; + return { x: 10, y: 10, zoom: 1 }; } catch (error) { - console.warn('Failed to load viewport from localStorage:', error); - return { [activeBoardIndex]: { x: 10, y: 10, zoom: 1.5 } }; + bugReporterManagerInsatance.addToLogs(95, String(error), 'MapView.tsx/loadViewport'); + return { x: 10, y: 10, zoom: 1 }; } }; - - // Load all storage data on mount and when activeBoardIndex changes + // Load all map data from the active board on mount and when activeBoardData changes useEffect(() => { setStorageLoaded(false); // Load and sanitize positions - const pos = loadPositions(); - const sanitizedPositions: Record = {}; - Object.keys(pos).forEach(id => { + const nodesData: nodeDataType = loadAllNodesData(); + const sanitizedPositions: Record = {}; + Object.keys(nodesData).forEach(id => { sanitizedPositions[id] = { - x: Number.isFinite(pos[id]?.x) ? pos[id].x : 0, - y: Number.isFinite(pos[id]?.y) ? pos[id].y : 0 - }; - }); - setPositions(sanitizedPositions); - - // Load and sanitize node sizes - const sizes = loadNodeSizes(); - const sanitizedSizes: Record = {}; - Object.keys(sizes).forEach(id => { - sanitizedSizes[id] = { - width: Number.isFinite(sizes[id]?.width) && sizes[id].width > 0 ? sizes[id].width : 300 + x: Number.isFinite(nodesData[id]?.x) ? nodesData[id].x : 0, + y: Number.isFinite(nodesData[id]?.y) ? nodesData[id].y : 0, + width: Number.isFinite(nodesData[id]?.width) ? nodesData[id].width : 300, }; }); - setNodeSizes(sanitizedSizes); - - // Load and sanitize viewport (board-wise) - const vpMap = loadViewport(); - const rawForBoard = vpMap[activeBoardIndex] || { x: 10, y: 10, zoom: 1.5 }; - const sanitizedForBoard: viewPort = { - x: Number.isFinite(rawForBoard.x) ? rawForBoard.x : 10, - y: Number.isFinite(rawForBoard.y) ? rawForBoard.y : 10, - zoom: Number.isFinite(rawForBoard.zoom) ? rawForBoard.zoom : 1.5 + // Update useRef instead of state to avoid re-render + allNodesData.current = sanitizedPositions; + + // Load and sanitize viewport + const rawVp = loadViewport(); + const sanitizedForBoard: viewPortType = { + x: Number.isFinite(rawVp.x) ? rawVp.x : 10, + y: Number.isFinite(rawVp.y) ? rawVp.y : 10, + zoom: Number.isFinite(rawVp.zoom) ? rawVp.zoom : 1.5 }; - setViewport(prev => ({ ...vpMap, [activeBoardIndex]: sanitizedForBoard })); + // setViewport(sanitizedForBoard); + viewPortData.current = sanitizedForBoard; setStorageLoaded(true); // Increment board change key to force initialNodes recalculation setBoardChangeKey(prev => prev + 1); - }, [activeBoardIndex]); + }, [activeBoardData]); + + + // ------------------------------------------------------------- + // EVENT EMITTERS FOR SAVING BOARD FILE + // ------------------------------------------------------------- + + useEffect(() => { + const saveMapDataListener = () => { + console.log("[Task Board] [MapView] SAVE_MAP signal fired...\nmapDataUpdated = ", mapDataUpdated.current); + if (!mapDataUpdated.current && !viewPortDataUpdated.current) return; + + let newBoardData = activeBoardData; + // Update the currentView's mapView data + // const viewIndex = newBoardData.views.findIndex(v => v.viewId === currentView.viewId); + if (currentViewIndex >= 0 && currentViewIndex < newBoardData.views.length) { + newBoardData.views[currentViewIndex].mapView = { + viewPortData: viewPortData.current, + nodesData: allNodesData.current, + }; + } + plugin.taskBoardFileManager.saveBoard(newBoardData); + + mapDataUpdated.current = false; + viewPortDataUpdated.current = false; + emitMapDataUpdatedSignal(false); + }; + + eventEmitter.on("SAVE_MAP", saveMapDataListener); + return () => eventEmitter.off("SAVE_MAP", saveMapDataListener); + }, [activeBoardData]); + + /** + * This function should be called when you want to emit a signal for + * other entities to know whether the map view is in UNSAVED sate or + * it the updated data has been properly saved. + * + * DONT CALL THIS FUNCTION WHEN VIEWPORT DATA HAS BEEN CHANGED. + * + * It also transmits a JSON data in the following form : { onlyviewport: boolean }. + * True -> Means, only the viewport data has been updated. (Viewport data is + * not considered to be a very important data in some scenarios, but still needs + * to be saved to board file.) + * False -> Means, other important map data has been updated along with + * the viewport data. + * + * @param flag - The flag tells what kind of signal to emit. + * True -> The map view is in unsaved state. + * False -> The map view has been saved to board file successfully. + */ + const emitMapDataUpdatedSignal = (flag: boolean) => { + if (flag) { + if (!mapDataUpdated.current) { + eventEmitter.emit("MAP_UNSAVED", { onlyviewport: false }); + mapDataUpdated.current = true; + } + } else { + eventEmitter.emit("MAP_SAVED"); + mapDataUpdated.current = false; + } + } // const reactFlowInstance = useReactFlow(); @@ -213,24 +230,85 @@ const MapView: React.FC = ({ // reactFlowInstance.setViewport({ x: positions[0].x, y: positions[0].y, zoom: 1 }); // }, []); + // ===== DEBUG: Selection monitoring component ===== + // This component monitors which nodes are actually selected by ReactFlow + // to debug the "ghost node" selection issue + // SOLUTION : This issue has been fixed, the root-cause of this issue was Numeric strings + // present in the tasks cache. So, when I was initializing the nodes, the numeric ID + // tasks were also getting initialized and it was causing this strange behavior. + // const MapViewSelectionDebugger: React.FC = () => { + // useOnSelectionChange({ + // onChange: (changes) => { + // const selectedNodeCount = changes.nodes?.length ?? 0; + // const selectedNodeIds = changes.nodes?.map((n) => n.id) ?? []; + // const selectedEdgeCount = changes.edges?.length ?? 0; + // const selectedEdgeIds = changes.edges?.map((e) => e.id) ?? []; + + // console.group('🔍 [MapView] Selection Change Debug Info'); + // console.log(`📊 Selected Nodes: ${selectedNodeCount}`); + // console.log('📝 Selected Node IDs:', selectedNodeIds); + // console.log(`📊 Selected Edges: ${selectedEdgeCount}`); + // console.log('📝 Selected Edge IDs:', selectedEdgeIds); + // console.log('🎯 Full Selection Changes Object:', changes); + // console.log('📋 Total nodes in map:', nodes.length); + // console.log('📋 All node IDs in map:', nodes.map(n => n.id)); + // console.groupEnd(); + + // // If selection count seems suspicious, log more details + // if (selectedNodeCount > (nodes?.length ?? 0) || selectedNodeCount < 0) { + // console.error( + // `⚠️ SUSPICIOUS: Selected ${selectedNodeCount} nodes, but only ${nodes?.length ?? 0} nodes exist in the map!`, + // changes + // ); + // } - // Kanban-style initial layout, memoized - const initialNodes: Node[] = useMemo(() => { - // Don't calculate nodes until storage data is loaded - if (!storageLoaded) { - return []; - } + // // Check for ghost nodes - nodes that are selected but don't exist in our nodes array + // const ghostNodes = selectedNodeIds.filter(selectedId => + // !nodes.some(node => node.id === selectedId) + // ); + // if (ghostNodes.length > 0) { + // console.error('👻 GHOST NODES DETECTED! Selected node IDs that don\'t exist in nodes array:', ghostNodes); + // } + + // // If user selected more nodes than available, log for investigation + // if (selectedNodeCount > 0) { + // const selectedNodeObjects = nodes?.filter((n) => selectedNodeIds.includes(n.id)) ?? []; + // console.log('🔎 Detailed info on selected nodes:', { + // nodesCount: selectedNodeObjects.length, + // nodes: selectedNodeObjects.map((n) => ({ + // id: n.id, + // position: n.position, + // width: n.width, + // selected: n.selected, + // })) + // }); + // } + // } + // }); + + // return null; // This component doesn't render anything + // }; + // ===== END DEBUG: Selection monitoring component ===== + + // ------------------------------------------------------------- + // ALL REACTFLOW INITIALIZATION CODE + // ------------------------------------------------------------- + // Kanban-style initial layout, memoized - now used for dynamic node updates + const computedNodes: Node[] = useMemo(() => { + // Don't calculate nodes until storage data is loaded + if (!storageLoaded) { return []; } const newNodes: Node[] = []; const usedIds = new Set(); const duplicateIds = new Set(); - const columnSpacing = 350; - const rowSpacing = 170; + const columnSpacing = 350; // base gap between columns + const rowSpacing = 200; + const tasksPerColumn = 20; // wrap after 20 tasks per column // Get default width with proper validation const getDefaultWidth = () => { try { - const columnWidth = plugin.settings.data.globalSettings.columnWidth; + const columnWidth = plugin.settings.data.columnWidth; if (!columnWidth || typeof columnWidth !== 'string') { return 300; // Fallback if missing or not a string } @@ -240,93 +318,118 @@ const MapView: React.FC = ({ return parsed; } } catch (e) { - console.warn('Error parsing columnWidth:', e); + bugReporterManagerInsatance.addToLogs(96, String(e), 'MapView.tsx/getDefaultWidth'); + return 300; // Fallback default width } return 300; // Fallback default width }; const defaultWidth = getDefaultWidth(); - let xOffset = 0; - allTasksArranged.forEach((columnTasks, colIdx) => { - let yOffset = 0; - columnTasks.forEach((task, rowIdx) => { - if (task.legacyId) { - const id = task.legacyId; - if (usedIds.has(id)) { - console.warn('Duplicate node id detected:', id, "\nTitle : ", task.title); - duplicateIds.add(id); - return; // Skip duplicate - } - usedIds.add(id); - const savedPos = positions[id] || {}; - const savedSize = nodeSizes[id] || {}; - - // Ensure width is always a valid number - let nodeWidth = defaultWidth; - if (savedSize.width && Number.isFinite(savedSize.width) && savedSize.width > 0) { - nodeWidth = savedSize.width; - } + // Counter for tasks that do NOT have saved positions so we can place them in a grid + let autoIndex = 0; + let maxColumnCount = 0; + + allTasksFlattened.forEach((task) => { + if (task.legacyId) { + /** + * We should cast this value to string cumpulsorily even though + * its already a string, else there is a possibility + * that atleast one of the id might be of type Numeric which can start + * causing the "GHOST NODE issue". + * + * @link - https://github.com/tu2-atmanand/Task-Board/issues/665 + */ + const id = String(task.legacyId); + if (usedIds.has(id)) { + duplicateIds.add(id); + return; + } + usedIds.add(id); - // Ensure positions are always valid finite numbers - const nodeX = Number.isFinite(savedPos.x) ? savedPos.x : xOffset; - const nodeY = Number.isFinite(savedPos.y) ? savedPos.y : yOffset; - - // Safety check: if computed offsets are somehow NaN, use fallback - const safeX = Number.isFinite(nodeX) ? nodeX : (colIdx * 350); - const safeY = Number.isFinite(nodeY) ? nodeY : (rowIdx * 170); - - newNodes.push({ - id, - type: 'ResizableNodeSelected', - data: { - label: - }, - position: { - x: safeX, - y: safeY - }, - width: nodeWidth, - }); - yOffset += rowSpacing; + const nodeData = allNodesData.current[id]; + + // Ensure width is always a valid number + let nodeWidth = defaultWidth; + if (nodeData?.width && Number.isFinite(nodeData.width) && nodeData.width > 0) { + nodeWidth = nodeData.width; } - }); - xOffset += columnSpacing; + // Determine position: use saved position if present and valid, else compute grid position + let posX: number; + let posY: number; + const hasSavedPos = nodeData && Number.isFinite(nodeData.x) && Number.isFinite(nodeData.y); + if (hasSavedPos) { + posX = nodeData.x; + posY = nodeData.y; + } else { + const columnIndex = Math.floor(autoIndex / tasksPerColumn); + const rowIndex = autoIndex % tasksPerColumn; + // Space columns by node width + columnSpacing so variable widths are respected + posX = columnIndex * (defaultWidth + columnSpacing); + posY = rowIndex * rowSpacing; + autoIndex += 1; + maxColumnCount = Math.max(maxColumnCount, columnIndex + 1); + } + + const safeX = Number.isFinite(posX) ? posX : 0; + const safeY = Number.isFinite(posY) ? posY : 0; + + newNodes.push({ + id, + type: 'ResizableNodeSelected', + data: { + label: + }, + position: { + x: safeX, + y: safeY, + }, + width: nodeWidth, + }); + } }); if (duplicateIds.size > 0) { const stringOfListOfDuplicateIds = Array.from(duplicateIds).join(','); - // bugReporterManagerInsatance.showNotice(17, `Following duplicate IDs has been found for tasks : "${stringOfListOfDuplicateIds}" detected in Map View. This may cause unexpected behavior. Please consider changing the IDs of these tasks.`, "ERROR: Same id is present on two tasks", "MapView.tsx/initialNodes"); + bugReporterManagerInsatance.showNotice(17, `Following duplicate IDs has been found for tasks with IDs: "${stringOfListOfDuplicateIds}". This may cause unexpected behavior. Please consider changing the IDs of these tasks.`, "ERROR: Same id is present on two tasks", "MapView.tsx/initialNodes"); duplicateIds.clear(); } return newNodes; - }, [allTasksArranged, activeBoardSettings, activeBoardIndex, storageLoaded, boardChangeKey]); + }, [allTasksFlattened, activeBoardData, storageLoaded, boardChangeKey]); + + // Manage nodes state - start with empty array as suggested by ReactFlow team + const [nodes, setNodes, onNodesChange] = useNodesState([]); - // Manage nodes state - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + // Update nodes dynamically when computedNodes changes (ReactFlow team suggestion) + useEffect(() => { + console.log("[MapView] Updating nodes via setNodes - computedNodes length:", computedNodes.length); + setNodes(computedNodes); + }, [computedNodes, setNodes]); // TODO : Its not a good idea to use debounce and allow stale data. I am already storing the data in localStorage on resize end in ResizableNodeSelected component. And there its much better as I can capture the final size directly based on the callback. - // Custom handler that intercepts dimension changes and updates nodeSizes state + // Custom handler that intercepts dimension changes and updates nodeSizeTypes state // const handleNodesChange = (changes: NodeChange[]) => { // // First, apply the changes to ReactFlow's state // onNodesChange(changes); - // updateSingleNodeSizeOnDiskDebounced(changes); + // updateSinglenodeSizeTypeOnDiskDebounced(changes); // }; - // const updateSingleNodeSizeOnDiskDebounced = debounce( + // const updateSinglenodeSizeTypeOnDiskDebounced = debounce( // async (changes: NodeChange[]): Promise => { // if (changes.length !== 1 || changes[0].type !== "dimensions") return; - // // Update nodeSizes state and localStorage - // const updatedSizes = { ...nodeSizes }; + // // Update nodeSizeTypes state and localStorage + // const updatedSizes = { ...nodeSizeTypes }; // let hasChanges = false; @@ -342,40 +445,40 @@ const MapView: React.FC = ({ // } // if (hasChanges) { - // // setNodeSizes(updatedSizes); + // // setnodeSizeTypes(updatedSizes); // try { // localStorage.setItem(NODE_SIZE_STORAGE_KEY, JSON.stringify(updatedSizes)); // } catch (error) { - // console.warn('Failed to save node sizes:', error); + // bugReporterManagerInsatance.addToLogs( + // 179, + // `Failed to save node sizes: ${String(error)}`, + // "MapView.tsx/updateSinglenodeSizeTypeOnDiskDebounced", + // ); // } // } // }, // 500 // ); - // Reset nodes when initialNodes changes - useEffect(() => { - setNodes(initialNodes); - }, [initialNodes, setNodes]); - // When the active board or viewport data changes, apply the stored viewport to the ReactFlow instance. - useEffect(() => { - const instance = reactFlowInstanceRef.current; - if (!instance) return; - const vpForBoard = viewport[activeBoardIndex]; - if (vpForBoard && Number.isFinite(vpForBoard.x) && Number.isFinite(vpForBoard.y) && Number.isFinite(vpForBoard.zoom)) { - try { - instance.setViewport(vpForBoard); - } catch (e) { - // ignore - instance may be transitioning - } - } - }, [activeBoardIndex, viewport]); + // useEffect(() => { + // const instance = reactFlowInstanceRef.current; + // if (!instance) return; + // const vpForBoard = viewport; + // if (vpForBoard && Number.isFinite(vpForBoard.x) && Number.isFinite(vpForBoard.y) && Number.isFinite(vpForBoard.zoom)) { + // try { + // instance.setViewport(vpForBoard); + // } catch (e) { + // // ignore - instance may be transitioning + // } + // } + // }, [viewport]); // Calculate edges from dependsOn property // TODO : Might be efficient to make use of the addEdge api of reactflow. function getEdgesFromTasks(): Edge[] { - const tasks: taskItem[] = allTasksArranged.flat(); + console.log("getEdgesFromTasks : How many times is this running and when..."); + const tasks: taskItem[] = allTasksFlattened; const edges: Edge[] = []; const idToTask = new Map(); tasks.forEach(task => { @@ -394,13 +497,13 @@ const MapView: React.FC = ({ // const cssMin = 1.2; // value when zoom is maxZ // const cssMax = 1.5; // value when zoom is minZ - const vpForBoard = viewport[activeBoardIndex] || { x: 10, y: 10, zoom: 1.5 }; - const zoom = Number.isFinite(vpForBoard?.zoom) ? vpForBoard.zoom : 1.5; - const clamped = Math.max(0.5, Math.min(2, zoom)); - const ratio = (clamped - 0.5) / (2 - 0.5); // 0..1 - const mapped = 1.5 - ratio * (1.5 - 1.2); - // Keep a compact string value suitable for CSS variable - const safeMarkerSize: number = Number.isFinite(mapped) ? 15 * mapped : 15; + // const vpForBoard = viewport || { x: 10, y: 10, zoom: 1.5 }; + // const zoom = Number.isFinite(vpForBoard?.zoom) ? vpForBoard.zoom : 1.5; + // const clamped = Math.max(0.5, Math.min(2, zoom)); + // const ratio = (clamped - 0.5) / (2 - 0.5); // 0..1 + // const mapped = 1.5 - ratio * (1.5 - 1.2); + // // Keep a compact string value suitable for CSS variable + // const safeMarkerSize: number = Number.isFinite(mapped) ? 15 * mapped : 15; tasks.forEach(task => { const sourceId = task.legacyId ? task.legacyId : String(task.id); @@ -425,15 +528,15 @@ const MapView: React.FC = ({ type: MarkerType.ArrowClosed, // required property // optional properties // color: 'var(--text-normal)', - height: (mapViewSettings.arrowDirection !== mapViewArrowDirection.childToParent && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, - width: (mapViewSettings.arrowDirection !== mapViewArrowDirection.childToParent && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, + // height: (mapViewSettings.arrowDirection !== mapViewArrowDirection.childToParent && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, + // width: (mapViewSettings.arrowDirection !== mapViewArrowDirection.childToParent && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, }, markerEnd: { type: MarkerType.ArrowClosed, // required property // optional properties // color: 'var(--text-normal)', - height: (mapViewSettings.arrowDirection !== mapViewArrowDirection.parentToChild && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, - width: (mapViewSettings.arrowDirection !== mapViewArrowDirection.parentToChild && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, + // height: (mapViewSettings.arrowDirection !== mapViewArrowDirection.parentToChild && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, + // width: (mapViewSettings.arrowDirection !== mapViewArrowDirection.parentToChild && Number.isFinite(safeMarkerSize)) ? safeMarkerSize : 0, }, }); } @@ -443,43 +546,33 @@ const MapView: React.FC = ({ }); return edges; } - const edges = useMemo(() => getEdgesFromTasks(), [allTasksArranged]); + const edges = useMemo(() => getEdgesFromTasks(), [allTasksFlattened]); + + // ------------------------------------------------------------- + // ALL EVENT HANDLING + // ------------------------------------------------------------- - const handleNodePositionChange = () => { - let allBoardPositions: Record> = {}; + const handlenodePositionChange = useCallback(() => { try { - const stored = localStorage.getItem(NODE_POSITIONS_STORAGE_KEY); - if (stored) { - allBoardPositions = JSON.parse(stored); - if (typeof allBoardPositions !== 'object' || allBoardPositions === null) { - allBoardPositions = {}; - } + // Update positions for current board with validation + const nodesDataMap: Record = {}; + for (const node of nodes) { + const x = Number.isFinite(node.position?.x) ? node.position.x : 0; + const y = Number.isFinite(node.position?.y) ? node.position.y : 0; + const width = Number.isFinite(node?.width) ? node.width ?? 300 : 300; + nodesDataMap[node.id] = { x, y, width }; } - } catch (error) { - console.warn('Failed to load existing positions:', error); - allBoardPositions = {}; - } - // Update positions for current board with validation - const posMap = nodes.reduce((acc, n) => { - const x = Number.isFinite(n.position?.x) ? n.position.x : 0; - const y = Number.isFinite(n.position?.y) ? n.position.y : 0; - acc[n.id] = { x, y }; - return acc; - }, {} as Record); - - setPositions(posMap); - allBoardPositions[String(activeBoardIndex)] = posMap; - - try { - localStorage.setItem(NODE_POSITIONS_STORAGE_KEY, JSON.stringify(allBoardPositions)); + // Only update useRef - no state update needed, avoiding re-render + allNodesData.current = nodesDataMap; + emitMapDataUpdatedSignal(true); } catch (error) { - console.warn('Failed to save node positions:', error); + bugReporterManagerInsatance.addToLogs(98, String(error), 'MapView.tsx/handlenodePositionTypeChange'); } - }; + }, [nodes, emitMapDataUpdatedSignal]); // Persist updated positions and sizes - // const prevNodeSizesRef = useRef>({}); + // const prevnodeSizeTypesRef = useRef>({}); // Only save sizes if they have changed // useEffect(() => { @@ -489,7 +582,7 @@ const MapView: React.FC = ({ // }, {} as Record); // // Compare with previous sizes - // const prevSizes = prevNodeSizesRef.current; + // const prevSizes = prevnodeSizeTypesRef.current; // let changed = false; // for (const id in sizeMap) { // if ( @@ -503,20 +596,19 @@ const MapView: React.FC = ({ // } // if (changed) { - // setNodeSizes(sizeMap); + // setnodeSizeTypes(sizeMap); // localStorage.setItem(NODE_SIZE_STORAGE_KEY, JSON.stringify(sizeMap)); - // prevNodeSizesRef.current = sizeMap; + // prevnodeSizeTypesRef.current = sizeMap; // } // }, [nodes]); // Handle edge creation (connecting nodes) const onConnect = useMemo<((params: Connection) => void)>(() => { - const flattenedTasks = allTasksArranged.flat(); + const flattenedTasks = allTasksFlattened; return (params: Connection) => { connectParentToChild(params.source, params.target, flattenedTasks); - // You may want to update the dependsOn property of the source task and trigger a re-render }; - }, [allTasksArranged]); + }, [allTasksFlattened]); // Function for connecting parent to child function connectParentToChild(sourceNodeId: string, targetNodeId: string, allTasks: taskItem[]) { @@ -546,7 +638,7 @@ const MapView: React.FC = ({ }; eventEmitter.emit("UPDATE_TASK", eventData); if (!isTaskNotePresentInTags(taskNoteIdentifierTag, updatedTargetTask.tags)) { - const updatedTargetTaskTitle = sanitizeDependsOn(plugin.settings.data.globalSettings, updatedTargetTask.title, updatedTargetTask.dependsOn); + const updatedTargetTaskTitle = sanitizeDependsOn(plugin.settings.data, updatedTargetTask.title, updatedTargetTask.dependsOn); updatedTargetTask.title = updatedTargetTaskTitle; // console.log('Updated source task :', updatedSourceTask, "\nOld source task:", sourceTask); @@ -614,44 +706,40 @@ const MapView: React.FC = ({ // }) as T; // } - // const throttledSetViewportStorage = throttle((vp: viewPort) => { + // const throttledSetViewportStorage = throttle((vp: viewPortType) => { // console.log('Saving viewport:', vp); // localStorage.setItem(VIEWPORT_STORAGE_KEY, JSON.stringify(vp)); // }, 20000); - const lastViewportSaveTime = useRef(0); - const debouncedSetViewportStorage = debounce((vp: viewPort) => { - const now = Date.now(); - if (now - lastViewportSaveTime.current > 2000) { + // const lastViewportSaveTime = useRef(0); + const debouncedSetViewportStorage = useCallback(debounce((vp: viewPortType) => { + // const now = Date.now(); + // if (now - lastViewportSaveTime.current > 2000) { + try { + if (!viewPortDataUpdated.current) { + eventEmitter.emit("MAP_UNSAVED", { onlyviewport: true }); + } + // Validate viewport values before saving - const safeViewport: viewPort = { + const safeViewport: viewPortType = { x: Number.isFinite(vp.x) ? vp.x : 10, y: Number.isFinite(vp.y) ? vp.y : 10, zoom: Number.isFinite(vp.zoom) && vp.zoom > 0 ? vp.zoom : 1.5 }; - try { - // Load existing map, update only current board entry - const stored = localStorage.getItem(VIEWPORT_STORAGE_KEY); - let parsed: Record = {}; - if (stored) { - try { - const p = JSON.parse(stored); - if (typeof p === 'object' && p !== null) parsed = p; - } catch (e) { - parsed = {}; - } - } - parsed[activeBoardIndex] = safeViewport; - localStorage.setItem(VIEWPORT_STORAGE_KEY, JSON.stringify(parsed)); - lastViewportSaveTime.current = now; - } catch (error) { - console.warn('Failed to save viewport:', error); - } + // Update the in-memory board object; persisting to disk will be handled by SAVE_MAP elsewhere + // setViewport(safeViewport); + viewPortData.current = safeViewport; + // emitMapDataUpdatedSignal(true); + viewPortDataUpdated.current = true; + // lastViewportSaveTime.current = now; + } catch (error) { + bugReporterManagerInsatance.addToLogs(99, String(error), 'MapView.tsx/debouncedSetViewportStorage'); } - }, 2000); + // } + }, 500), []); - const handlePaneContextMenu = (event: MouseEvent | React.MouseEvent) => { + const handlePaneContextMenu = useCallback((event: MouseEvent | React.MouseEvent) => { const sortMenu = new Menu(); @@ -681,7 +769,7 @@ const MapView: React.FC = ({ item.setTitle(t("transparent")); item.setIcon("eye-off"); item.onClick(() => { - plugin.settings.data.globalSettings.mapView.background = mapViewBackgrounVariantTypes.transparent; + plugin.settings.data.mapView.background = mapViewBackgrounVariantTypes.transparent; plugin.saveSettings(); // Refresh the board view @@ -694,7 +782,7 @@ const MapView: React.FC = ({ item.setTitle(t("dots")); item.setIcon("grip"); item.onClick(() => { - plugin.settings.data.globalSettings.mapView.background = mapViewBackgrounVariantTypes.dots; + plugin.settings.data.mapView.background = mapViewBackgrounVariantTypes.dots; plugin.saveSettings(); eventEmitter.emit('REFRESH_BOARD'); @@ -706,7 +794,7 @@ const MapView: React.FC = ({ item.setTitle(t("lines")); item.setIcon("grid-3x3"); item.onClick(() => { - plugin.settings.data.globalSettings.mapView.background = mapViewBackgrounVariantTypes.lines; + plugin.settings.data.mapView.background = mapViewBackgrounVariantTypes.lines; plugin.saveSettings(); eventEmitter.emit('REFRESH_BOARD'); @@ -718,7 +806,7 @@ const MapView: React.FC = ({ item.setTitle(t("cross")); item.setIcon("x"); item.onClick(() => { - plugin.settings.data.globalSettings.mapView.background = mapViewBackgrounVariantTypes.cross; + plugin.settings.data.mapView.background = mapViewBackgrounVariantTypes.cross; plugin.saveSettings(); eventEmitter.emit('REFRESH_BOARD'); @@ -732,7 +820,7 @@ const MapView: React.FC = ({ item.setTitle(t("show-minimap")); item.setIcon("map"); item.onClick(async () => { - plugin.settings.data.globalSettings.mapView.showMinimap = !plugin.settings.data.globalSettings.mapView.showMinimap; + plugin.settings.data.mapView.showMinimap = !plugin.settings.data.mapView.showMinimap; plugin.saveSettings(); eventEmitter.emit('REFRESH_BOARD'); @@ -744,7 +832,7 @@ const MapView: React.FC = ({ item.setTitle(t("animate-links")); item.setIcon("worm"); item.onClick(async () => { - plugin.settings.data.globalSettings.mapView.animatedEdges = !plugin.settings.data.globalSettings.mapView.animatedEdges; + plugin.settings.data.mapView.animatedEdges = !plugin.settings.data.mapView.animatedEdges; plugin.saveSettings(); eventEmitter.emit('REFRESH_BOARD'); @@ -756,7 +844,7 @@ const MapView: React.FC = ({ sortMenu.showAtMouseEvent( (event instanceof MouseEvent ? event : event.nativeEvent) ); - } + }, [mapViewSettings.background, mapViewSettings.showMinimap, mapViewSettings.animatedEdges]); // Will implement the below function if required in future. // const handleOnDragOver = () => { @@ -773,11 +861,11 @@ const MapView: React.FC = ({ // node.selected = false; // } - const toggleTasksImporterPanel = () => { + const toggleTasksImporterPanel = useCallback(() => { setIsImporterPanelVisible(prev => !prev); - } + }, []); - const handleEdgeClick = (event: any, edge: Edge) => { + const handleEdgeClick = useCallback((event: any, edge: Edge) => { // Show Obsidian menu for the selected edge const menu = new Menu(); menu.addItem((item) => { @@ -786,7 +874,7 @@ const MapView: React.FC = ({ item.onClick(async () => { // Edge id format: `${targetId}->${sourceId}` const [targetId, sourceId] = edge.id.split('->'); - const allTasks = allTasksArranged.flat(); + const allTasks = allTasksFlattened; const targetTask = allTasks.find(t => (t.legacyId ? t.legacyId : String(t.id)) === targetId); if (!targetTask) { bugReporterManagerInsatance.showNotice(18, "The parent task was not found in the cache. Maybe the ID didnt match or the task itself was not present in the file. Or the file has been moved to a different location.", `Parent task id : ${targetId}\nChild task id : ${sourceId}`, "MapView.tsx/handleEdgeClick"); @@ -798,7 +886,7 @@ const MapView: React.FC = ({ return; } - const updatedDependsOn = targetTask.dependsOn.filter(dep => dep !== sourceId); + const updatedDependsOn = targetTask.dependsOn.filter((dep: string) => dep !== sourceId); const updatedTargetTask = { ...targetTask, dependsOn: updatedDependsOn @@ -812,7 +900,7 @@ const MapView: React.FC = ({ try { if (!isTaskNotePresentInTags(taskNoteIdentifierTag, updatedTargetTask.tags)) { - const updatedTargetTaskTitle = sanitizeDependsOn(plugin.settings.data.globalSettings, updatedTargetTask.title, updatedTargetTask.dependsOn); + const updatedTargetTaskTitle = sanitizeDependsOn(plugin.settings.data, updatedTargetTask.title, updatedTargetTask.dependsOn); updatedTargetTask.title = updatedTargetTaskTitle; await updateTaskInFile(plugin, updatedTargetTask, targetTask); @@ -872,10 +960,14 @@ const MapView: React.FC = ({ }); menu.showAtMouseEvent(event instanceof MouseEvent ? event : event.nativeEvent); - } + }, [allTasksFlattened, taskNoteIdentifierTag, plugin]); - if (allTasksArranged.flat().length === 0) { + // ------------------------------------------------------------- + // ALL RENDERING CODE + // ------------------------------------------------------------- + + if (allTasksFlattened.length === 0) { return (
@@ -885,7 +977,7 @@ const MapView: React.FC = ({
); - } else if (storageLoaded && initialNodes.length === 0) { + } else if (storageLoaded && computedNodes.length === 0) { return (
@@ -901,8 +993,10 @@ const MapView: React.FC = ({ setIsImporterPanelVisible(false)} /> @@ -938,7 +1032,7 @@ const MapView: React.FC = ({ // const cssMin = 1; // value when zoom is maxZ // const cssMax = 2; // value when zoom is minZ - const vpForBoard = viewport[activeBoardIndex] || { x: 10, y: 10, zoom: 1.5 }; + const vpForBoard = viewPortData.current || { x: 10, y: 10, zoom: 1.5 }; const z = Number.isFinite(vpForBoard?.zoom) ? vpForBoard.zoom : 1.5; const clamped = Math.max(0.5, Math.min(2, z)); const ratio = (clamped - 0.5) / (2 - 0.5); // 0..1 @@ -958,8 +1052,9 @@ const MapView: React.FC = ({ nodeTypes={nodeTypes} onEdgeClick={handleEdgeClick} onNodesChange={onNodesChange} - onNodeDragStop={() => { - handleNodePositionChange(); + onNodeDragStop={(node) => { + console.log("Following node position has been updated : ", node); + handlenodePositionChange(); }} // viewport controls @@ -990,16 +1085,17 @@ const MapView: React.FC = ({ // rendering onlyRenderVisibleElements={mapViewSettings.renderVisibleNodes} // TODO : If this is true, then the initial render is faster, but while panning the experience is little laggy. onInit={(instance) => { + console.log("Reactflow Initializing..."); // store reactflow instance for later programmatic viewport updates - try { - reactFlowInstanceRef.current = instance; - } catch (e) { - // ignore - } + // try { + // reactFlowInstanceRef.current = instance; + // } catch (e) { + // // ignore + // } if (focusOnTaskId) { const node = nodes.find(n => n.id === focusOnTaskId); if (node && Number.isFinite(node.position.x) && Number.isFinite(node.position.y)) { - const newVp: viewPort = { + const newVp: viewPortType = { x: - (node.position.x - 200), y: - (node.position.y), zoom: 1 @@ -1007,23 +1103,25 @@ const MapView: React.FC = ({ // Validate the new viewport before setting if (Number.isFinite(newVp.x) && Number.isFinite(newVp.y) && Number.isFinite(newVp.zoom)) { instance.setViewport(newVp); - setViewport(prev => ({ ...prev, [activeBoardIndex]: newVp })); + // setViewport(newVp); + viewPortData.current = newVp; debouncedSetViewportStorage(newVp); return; } } } // Use current viewport if valid for this board, otherwise fall back to defaults - const currentVpForBoard = viewport[activeBoardIndex]; + const currentVpForBoard = viewPortData.current; if (currentVpForBoard && Number.isFinite(currentVpForBoard.x) && Number.isFinite(currentVpForBoard.y) && Number.isFinite(currentVpForBoard.zoom) && currentVpForBoard.zoom > 0) { instance.setViewport(currentVpForBoard); } else { - const defaultVp: viewPort = { x: 10, y: 10, zoom: 1.5 }; + const defaultVp: viewPortType = { x: 10, y: 10, zoom: 1.5 }; instance.setViewport(defaultVp); - setViewport(prev => ({ ...prev, [activeBoardIndex]: defaultVp })); + // setViewport(defaultVp); + viewPortData.current = defaultVp; } }} - defaultViewport={viewport[activeBoardIndex]} + defaultViewport={viewPortData.current} elevateEdgesOnSelect={true} > @@ -1039,12 +1137,17 @@ const MapView: React.FC = ({ )} + + {/* DEBUG: Selection monitoring */} + {/* */} setIsImporterPanelVisible(false)} /> diff --git a/src/components/MapView/MapViewMinimap.tsx b/src/components/MapView/MapViewMinimap.tsx index 57f60298..c0f71ea3 100644 --- a/src/components/MapView/MapViewMinimap.tsx +++ b/src/components/MapView/MapViewMinimap.tsx @@ -1,8 +1,8 @@ import React from "react"; import { MiniMap, Node } from "@xyflow/react"; -import { TagColor } from "src/interfaces/GlobalSettings"; -import { taskItem } from "src/interfaces/TaskItem"; -import { matchTagsWithWildcards } from "src/utils/algorithms/ScanningFilterer"; +import { TagColor } from "../../interfaces/GlobalSettings.js"; +import { taskItem } from "../../interfaces/TaskItem.js"; +import { matchTagsWithWildcards } from "../../utils/algorithms/ScanningFilterer.js"; export interface CustomNode extends Node { data: { label: { props: { task: taskItem } } } diff --git a/src/components/MapView/ResizableNodeSelected.tsx b/src/components/MapView/ResizableNodeSelected.tsx index 61f2bbc8..0c13aaf0 100644 --- a/src/components/MapView/ResizableNodeSelected.tsx +++ b/src/components/MapView/ResizableNodeSelected.tsx @@ -1,11 +1,11 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeResizer, NodeProps } from '@xyflow/react'; -import { nodeSize } from './MapView'; -import { NODE_SIZE_STORAGE_KEY } from 'src/interfaces/Constants'; -import type TaskBoard from 'main'; -import { mapViewNodeMapOrientation } from 'src/interfaces/Enums'; -import { CircleArrowDownIcon, CircleArrowRightIcon } from 'lucide-react'; -import { t } from 'src/utils/lang/helper'; +import { t } from 'i18next'; +import { CircleArrowRightIcon, CircleArrowDownIcon } from 'lucide-react'; +import TaskBoard from '../../../main.js'; +import { mapViewNodeMapOrientation } from '../../interfaces/Enums.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +// import { nodeSize } from './MapView'; interface dataProps extends React.ReactElement { props: { plugin: TaskBoard }; @@ -17,7 +17,7 @@ interface ResizableNodeSelectedProps { } const ResizableNodeSelected: FC = ({ id, data, selected, width, height }) => { - const mapViewSettings = data.label?.props?.plugin.settings.data.globalSettings.mapView; + const mapViewSettings = data.label?.props?.plugin.settings.data.mapView; const orientationHorizontal = mapViewSettings.mapOrientation === mapViewNodeMapOrientation.horizontal; // console.log('Rendering ResizableNodeSelected for node:', id, { data, selected, width, height }); @@ -34,14 +34,19 @@ const ResizableNodeSelected: FC = ({ id, onResizeEnd={(newSize, params) => { // console.log('Node resized to:', newSize, "\nparams:", params, "\nNode ID:", id); try { - const sizeData: Record = JSON.parse(localStorage.getItem(NODE_SIZE_STORAGE_KEY) || '{}'); - sizeData[id] = { - width: params.width ?? data.label.props.plugin.settings.data.globalSettings.columnWidth ?? 300 - // height: params.height ?? 30 - }; - localStorage.setItem(NODE_SIZE_STORAGE_KEY, JSON.stringify(sizeData)); + console.log("Data :", data); + // const sizeData: Record = JSON.parse(localStorage.getItem(NODE_SIZE_STORAGE_KEY) || '{}'); + // sizeData[id] = { + // width: params.width ?? data.label.props.plugin.settings.data.columnWidth ?? 300 + // // height: params.height ?? 30 + // }; + // localStorage.setItem(NODE_SIZE_STORAGE_KEY, JSON.stringify(sizeData)); } catch (e) { - console.error('Failed to update node size in localStorage:', e); + bugReporterManagerInsatance.addToLogs( + 127, + String(e), + "ResizableNodeSelected.tsx/return()", + ); } }} /> diff --git a/src/components/MapView/TasksImporterPanel.tsx b/src/components/MapView/TasksImporterPanel.tsx index 4915df9b..9f5af2ff 100644 --- a/src/components/MapView/TasksImporterPanel.tsx +++ b/src/components/MapView/TasksImporterPanel.tsx @@ -1,19 +1,23 @@ // /src/components/MapView/TasksImporterPanel.tsx -import React, { useState, useEffect, useMemo } from 'react'; -import { X } from 'lucide-react'; -import { taskItem } from 'src/interfaces/TaskItem'; -import TaskItem from '../KanbanView/TaskItem'; -import TaskBoard from 'main'; -import { Board } from 'src/interfaces/BoardConfigs'; -import { t } from 'src/utils/lang/helper'; -import { eventEmitter } from 'src/services/EventEmitter'; -import { applyIdToTaskItem } from 'src/utils/TaskItemUtils'; +import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { X, ChevronDown } from 'lucide-react'; +import TaskBoard from '../../../main.js'; +import { Board, TaskBoardViewType } from '../../interfaces/BoardConfigs.js'; +import { viewTypeNames } from '../../interfaces/Enums.js'; +import { taskItem } from '../../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +import { eventEmitter } from '../../services/EventEmitter.js'; +import { applyIdToTaskItem } from '../../utils/TaskItemUtils.js'; +import TaskItem from '../TaskCard/TaskItem.js'; +import { t } from '../../utils/lang/helper.js'; interface TasksImporterPanelProps { plugin: TaskBoard; allTasksArranged: taskItem[][]; activeBoardSettings: Board; + activeViewData: TaskBoardViewType; + activeViewIndex: number; isVisible: boolean; onClose: () => void; } @@ -22,11 +26,23 @@ export const TasksImporterPanel: React.FC = ({ plugin, allTasksArranged, activeBoardSettings, + activeViewData, + activeViewIndex, isVisible, onClose }) => { const [searchQuery, setSearchQuery] = useState(''); const [importedTaskIds, setImportedTaskIds] = useState>(new Set()); + const tasksContentRef = useRef(null); + + // Lazy loading configs + // Lazy loading configs + const initialTaskCount = 20; + const loadMoreCount = 10; + const scrollThresholdPercent = 80; + + // State for managing visible tasks + const [visibleTaskCount, setVisibleTaskCount] = useState(initialTaskCount); // Get all tasks without an ID (legacyId is empty) const tasksWithoutId = useMemo(() => { @@ -47,6 +63,12 @@ export const TasksImporterPanel: React.FC = ({ ); }, [tasksWithoutId, searchQuery]); + // Memoize visible tasks based on count + const visibleTasks = useMemo(() => { + if (!filteredTasks || filteredTasks.length < 1) return []; + return filteredTasks.slice(0, visibleTaskCount); + }, [filteredTasks, visibleTaskCount]); + const handleImportTask = async (task: taskItem) => { try { const newId = await applyIdToTaskItem(plugin, task); @@ -57,15 +79,64 @@ export const TasksImporterPanel: React.FC = ({ sleep(500).then(async () => { await plugin.realTimeScanner.processAllUpdatedFiles(task.filePath); - // Emit event to refresh the board + // Emit event to refresh the board and notify about the newly imported task + eventEmitter.emit('TASK_IMPORTED_TO_MAP', newId); eventEmitter.emit('REFRESH_BOARD'); // TODO : Will this work with REFRESH_COLUMN only. }) } } catch (error) { - console.error('Error importing task:', error); + bugReporterManagerInsatance.addToLogs( + 128, + String(error), + "TasksImporterPanel.tsx/handleImportTask", + ); } }; + // Reset visible count when filtered tasks change (e.g., due to search) + useEffect(() => { + setVisibleTaskCount(initialTaskCount); + }, [filteredTasks, initialTaskCount]); + + // Scroll event handler + const handleScroll = useCallback(() => { + const container = tasksContentRef.current; + if (!container) return; + + const { scrollTop, scrollHeight, clientHeight } = container; + const scrollPercentage = ((scrollTop + clientHeight) / scrollHeight) * 100; + + // Load more tasks when scroll threshold is reached and there are more tasks to load + if (scrollPercentage >= scrollThresholdPercent && visibleTaskCount < filteredTasks.length) { + setVisibleTaskCount((prevCount) => { + const newCount = Math.min(prevCount + loadMoreCount, filteredTasks.length); + return newCount; + }); + } + }, [scrollThresholdPercent, visibleTaskCount, filteredTasks.length, loadMoreCount]); + + // Attach scroll listener + useEffect(() => { + const container = tasksContentRef.current; + if (!container) return; + + // Throttle scroll events for performance + let throttleTimeout: NodeJS.Timeout | null = null; + const throttledScroll = () => { + if (throttleTimeout) return; + throttleTimeout = setTimeout(() => { + handleScroll(); + throttleTimeout = null; + }, 100); + }; + + container.addEventListener('scroll', throttledScroll); + return () => { + container.removeEventListener('scroll', throttledScroll); + if (throttleTimeout) clearTimeout(throttleTimeout); + }; + }, [handleScroll]); + // Reset imported tasks when panel is closed useEffect(() => { if (!isVisible) { @@ -128,7 +199,7 @@ export const TasksImporterPanel: React.FC = ({ )}
-
+
{filteredTasks.length === 0 ? (

@@ -138,23 +209,36 @@ export const TasksImporterPanel: React.FC = ({

) : ( -
- {filteredTasks.map((task, index) => ( -
handleImportTask(task)} - > - +
+ {visibleTasks.map((task, index) => ( +
-
- ))} -
+ className="tasksImporterPanelTaskItemWrapper" + onClick={() => handleImportTask(task)} + > + +
+ ))} +
+ {visibleTaskCount < filteredTasks.length && ( + + )} + )}
diff --git a/src/components/TaskBoardEmbedComponent.tsx b/src/components/TaskBoardEmbedComponent.tsx new file mode 100644 index 00000000..4b39818d --- /dev/null +++ b/src/components/TaskBoardEmbedComponent.tsx @@ -0,0 +1,58 @@ +import { Component, TFile } from "obsidian"; +import { Root, createRoot } from "react-dom/client"; +import { StrictMode } from "react"; +import TaskBoard from "../../main.js"; +import TaskBoardViewContainer from "./TaskBoardViewContainer.js"; +import { bugReporterManagerInsatance } from "../managers/BugReporter.js"; + +export class TaskBoardEmbedComponent extends Component { + plugin: TaskBoard; + file: TFile; + linkText?: string; + root: Root | null = null; + protected contentEl: HTMLElement; + + constructor(contentEl: HTMLElement, plugin: TaskBoard, file: TFile, linkText?: string) { + super(); + this.contentEl = contentEl; + this.contentEl.addClass("task-board-embed"); + this.plugin = plugin; + this.file = file; + this.linkText = linkText; + } + + async loadFile() { + try { + const boardData = await this.plugin.taskBoardFileManager.loadBoardUsingPath(this.file.path); + if (boardData) { + this.root = createRoot(this.contentEl); + this.root.render( + + + + ); + } else { + this.contentEl.createEl("div", { text: "Failed to load task board" }); + } + } catch (error) { + bugReporterManagerInsatance.addToLogs( + 196, + `Error loading task board for embed: ${error}`, + "TaskBoardEmbedComponent.tsx/loadFile", + ); + this.contentEl.createEl("div", { text: "Error loading task board" }); + } + } + + onunload() { + if (this.root) { + this.root.unmount(); + this.root = null; + } + super.onunload(); + } +} diff --git a/src/components/TaskBoardViewContainer.tsx b/src/components/TaskBoardViewContainer.tsx new file mode 100644 index 00000000..59916825 --- /dev/null +++ b/src/components/TaskBoardViewContainer.tsx @@ -0,0 +1,1387 @@ +// src/components/TaskBoardViewContainer.tsx + +import { CirclePlus, RefreshCcw, Search, SearchX, Filter, Settings, EllipsisVertical, List, Network, BrickWall, SquareKanban, Save, PanelLeft, ChevronDownIcon } from 'lucide-react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { debounce, Platform, Menu, WorkspaceLeaf } from "obsidian"; +import { t } from 'i18next'; +import TaskBoard from '../../main.js'; +import { Board, RootFilterState, TaskBoardViewType } from '../interfaces/BoardConfigs.js'; +import { DEFAULT_DATE_FORMAT } from '../interfaces/Constants.js'; +import { taskPropertiesNames, viewTypeNames, viewsPanelPropertiesToShow } from '../interfaces/Enums.js'; +import { funnelIcon, ScanVaultIcon } from '../interfaces/Icons.js'; +import { taskJsonMerged } from '../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../managers/BugReporter.js'; +import { eventEmitter } from '../services/EventEmitter.js'; +import { openBoardConfigModal, openAddNewTaskModal, openScanVaultModal } from '../services/OpenModals.js'; +import { advancedFilterer } from '../utils/algorithms/AdvancedFilterer.js'; +import { loadTasksAndMerge } from '../utils/JsonFileOperations.js'; +import { getViewIndex, getViewById } from '../utils/ViewUtils.js'; +import { AdvancedFilterModal } from './AdvancedFilterer/index.js'; +import { AdvancedFilterPopover } from './AdvancedFilterer/Popover.js'; +import MapView from './MapView/MapView.js'; +import KanbanBoardView from './KanbanView/KanbanBoardView.js'; + +const TaskBoardViewContainer: React.FC<{ plugin: TaskBoard, currentBoardData: Board, currentLeaf?: WorkspaceLeaf }> = ({ plugin, currentBoardData, currentLeaf }) => { + const [boardData, setCurrentBoardData] = useState(currentBoardData); + const [currentViewIndex, setCurrentViewIndex] = useState(0); + const [currentView, setCurrentView] = useState(() => { + const initialBoard = currentBoardData; + if (initialBoard?.views?.length > 0) { + const lastViewIndex = getViewIndex(initialBoard, initialBoard.lastViewId); + if (lastViewIndex !== -1) { + setCurrentViewIndex(lastViewIndex); + return initialBoard.views[lastViewIndex]; + } + + return initialBoard.views[0]; + } + return undefined; + }); + + // All UI Refs + const [isResizing, setIsResizing] = useState(false); + const drawerRef = useRef(null); + const filterPopoverRef = useRef(null); + const [loading, setLoading] = useState(true); + const [showSearchInput, setShowSearchInput] = useState(plugin.settings.data.searchQuery ? true : false); + const [viewWidth, setviewWidth] = useState(currentLeaf ? currentLeaf.width : 800); + const [sidebarAnimating, setSidebarAnimating] = useState(false); + + const [allTasks, setAllTasks] = useState(); + const [filteredTasks, setFilteredTasks] = useState(null); + const [refreshCount, setRefreshCount] = useState(0); + const [freshInstall, setFreshInstall] = useState(false); + const [searchQuery, setSearchQuery] = useState(plugin.settings.data.searchQuery ?? ""); + const [showAllElements, setShowAllElements] = useState(true); // show elements for screens larger than 1000px + const [isMobileView, setIsMobileView] = useState(false); // show elements for screens smaller than 800px + const [boardDrawerVisible, setboardDrawerVisible] = useState(boardData.viewsPanel.isOpen); + const [editorModified, setEditorModified] = useState(plugin.editorModified); + + /** + * These below two states are a temporary solution using eventEmitters for now which is working fine. + * In reality, the map view should update the boardData inside the {@link taskBoardFileManager} + * cache, just like how Obsidian does an atomic transaction operations. In this scenario, if + * in the future, this plugin has to do atomic changes to the same board file instantly, + * the data can be simply updated in the cache and then after sometime it will be + * written to the disk. + */ + const [mapViewDataUpdated, setMapViewDataUpdated] = useState(false); + const viewPortDataOfMapViewUpdated = useRef(false); + + // // Derive current view from board data and currentViewId + // const currentView: View | undefined = useMemo(() => { + // if (boardData) { + // return getViewById(boardData, currentViewId); + // } + // return undefined; + // }, [boardData, currentViewId]); + + useEffect(() => { + const handleResize = () => { + const taskBoardLeaf = currentLeaf; + if (taskBoardLeaf) { + setviewWidth(taskBoardLeaf.width); + document.documentElement.style.setProperty('--taskboard-leaf-width', `${taskBoardLeaf.width}px`); + } + }; + + handleResize(); + if (currentLeaf) { + plugin.registerEvent(plugin.app.workspace.on("resize", handleResize)); + } + return () => { + // cleanup if needed + }; + }, []); + + useEffect(() => { + setShowAllElements(viewWidth >= 1000); + setIsMobileView(viewWidth <= 800); // For even little bigger screen smartphones, let go with 800 + }, [viewWidth]); + + // Update currentView when currentViewIndex or boardData changes + useEffect(() => { + if (boardData?.views && currentViewIndex >= 0 && currentViewIndex < boardData.views.length) { + const newView = boardData.views[currentViewIndex]; + setCurrentView(newView); + } + }, [currentViewIndex, boardData?.views]); + + useEffect(() => { + const fetchData = async () => { + console.log("TASK BOARD : Does this run while switching boards..."); + try { + // if (currentBoardData) { + setCurrentBoardData(currentBoardData); + setCurrentView(currentBoardData.views[currentViewIndex]); + + // // Get index of the new board from the registry based on the board id. + // const indexOfNewBoard = plugin.taskBoardFileManager.getBoardIndexFromRegistry(currentBoardData.id);; + // const registryLength = Object.keys(plugin.settings.data.taskBoardFilesRegistry || {}).length; setActiveBoardIndex(indexOfNewBoard ?? registryLength); + + // // When board changes, automatically select the first view if available + // if (currentBoardData?.views?.length > 0) { + // const firstViewId = currentBoardData.views[0].viewId; + // setCurrentViewId(firstViewId); + // plugin.settings.data.lastViewHistory.currentViewId = firstViewId; + // } + // } else { + // const data = await plugin.taskBoardFileManager.loadBoardUsingIndex(activeBoardIndex); + // if (!data) throw "Board data not found."; + + // setCurrentBoardData(data); + // } + + const allTasks = await loadTasksAndMerge(plugin, true); + if (allTasks) { + setAllTasks(allTasks); + setFreshInstall(false); + } + } catch (error) { + bugReporterManagerInsatance.addToLogs( + 131, + `No need to worry about this bug, if its appearing on the fresh install.\n${String(error)}`, + "TaskBoardViewContainer.tsx/loading boards and tasks useEffect", + ); + setFreshInstall(true); + } + }; + + fetchData(); + }, [refreshCount]); + + // First memo: Filter tasks by board filter and search query (but don't segregate by column yet) + const filteredAndSearchedTasks = useMemo(() => { + if (allTasks && currentView) { + const viewFilter = currentView.viewFilter; + const dateFormat = plugin.settings.data.dateFormat || DEFAULT_DATE_FORMAT; + + // Apply board filters to tasks + const boardFilteredTasks = { + ...allTasks, + Pending: advancedFilterer(allTasks.Pending, viewFilter, dateFormat), + Completed: advancedFilterer(allTasks.Completed, viewFilter, dateFormat), + }; + + let newViewdData = currentView; + // Update task count in settings + newViewdData.taskCount = { + pending: boardFilteredTasks.Pending.length, + completed: boardFilteredTasks.Completed.length, + }; + setCurrentView(newViewdData); + setFilteredTasks(boardFilteredTasks); + + // Apply search filter if search query exists + if (searchQuery.trim() !== "") { + const searchFiltered = handleSearchSubmit(boardFilteredTasks); + // setLoading(false); + return searchFiltered || boardFilteredTasks; + } + + // setLoading(false); + return boardFilteredTasks; + } + return { Pending: [], Completed: [] }; + }, [allTasks, currentBoardData, searchQuery]); + + useEffect(() => { + if (filteredAndSearchedTasks.Pending.length > 0 || filteredAndSearchedTasks.Completed.length > 0) { + setLoading(false); + } + }, [filteredAndSearchedTasks]); + + const debouncedRefreshColumn = useCallback( + debounce(async () => { + try { + const allTasks = await loadTasksAndMerge(plugin, false); + setAllTasks(allTasks); + } catch (error) { + bugReporterManagerInsatance.showNotice(28, "Error loading tasks on column refresh", String(error), "TaskBoardViewContainer.tsx/debouncedRefreshColumn"); + } + }, 500), + [plugin] + ); + + useEffect(() => { + eventEmitter.on("REFRESH_COLUMN", debouncedRefreshColumn); + return () => eventEmitter.off("REFRESH_COLUMN", debouncedRefreshColumn); + }, [debouncedRefreshColumn]); + + useEffect(() => { + const refreshBoardListener = () => setRefreshCount((prev) => prev + 1); + eventEmitter.on("REFRESH_BOARD", refreshBoardListener); + return () => eventEmitter.off("REFRESH_BOARD", refreshBoardListener); + }, []); + + useEffect(() => { + const handleMapDataUpdated = (eventData: { onlyviewport: boolean }) => { + if (eventData.onlyviewport) + viewPortDataOfMapViewUpdated.current = true; + else + setMapViewDataUpdated(true); + }; + + const handleMapDataSaved = () => { + setMapViewDataUpdated(false); + viewPortDataOfMapViewUpdated.current = false; + + } + + eventEmitter.on("MAP_UNSAVED", handleMapDataUpdated); + eventEmitter.on("MAP_SAVED", handleMapDataSaved); + return () => { + eventEmitter.off("MAP_UNSAVED", handleMapDataUpdated); + eventEmitter.off("MAP_SAVED", handleMapDataSaved); + }; + }, []); + + useEffect(() => { + const refreshView = (viewId: string) => { + setCurrentView(getViewById(boardData, viewId)); + + let updatedBoardData = boardData; + updatedBoardData.lastViewId = viewId; + plugin.taskBoardFileManager.saveBoard(updatedBoardData); + }; + eventEmitter.on("SWITCH_VIEW", refreshView); + return () => eventEmitter.off("SWITCH_VIEW", refreshView); + }, []); + + // Listen to editor modified state changes + useEffect(() => { + const handleEditorModifiedChange = (modified: boolean) => { + setEditorModified(modified); + }; + + eventEmitter.on("EDITOR_MODIFIED_CHANGED", handleEditorModifiedChange); + return () => eventEmitter.off("EDITOR_MODIFIED_CHANGED", handleEditorModifiedChange); + }, []); + + const refreshBoardButton = useCallback(() => { + plugin.realTimeScanner.processAllUpdatedFiles(); //.then(() => console.log("Finished processing all updated files.")); + plugin.processCreateQueue(); //.then(() => console.log("Finished processing create queue.")); + plugin.processDeleteQueue(); //.then(() => console.log("Finished processing delete queue.")); + plugin.processRenameQueue(); //.then(() => console.log("Finished processing rename queue.")); + + setTimeout(() => { + eventEmitter.emit("REFRESH_BOARD"); + }, 100); + }, []); + + // function handleOpenTaskBoardActionsModal() { + // if (boardData) + // openTaskBoardActionsModal(plugin, boardData); + // } + + function handleSearchButtonClick() { + if (showSearchInput) { + setSearchQuery(""); + // el.currentTarget.focus(); + plugin.settings.data.searchQuery = ""; + + eventEmitter.emit("REFRESH_COLUMN"); + plugin.saveSettings(); + setShowSearchInput(false); + } else { + setSearchQuery(plugin.settings.data.searchQuery || ""); + handleSearchSubmit(); + setShowSearchInput(true); + } + } + + // function highlightMatch(text: string, query: string): string { + // const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // const regex = new RegExp(`(${escapedQuery})`, "gi"); + // return text.replace(regex, `$1`); + // } + + function handleSearchSubmit(fileteredAllTasks?: taskJsonMerged): taskJsonMerged | null { + if (!searchQuery.trim()) { + return null; + } + + const lowerQuery = searchQuery.toLowerCase(); + let searchFilteredTasks: taskJsonMerged | null = null; + + if (fileteredAllTasks) { + searchFilteredTasks = { + Pending: fileteredAllTasks.Pending.filter((task) => { + if (lowerQuery.startsWith("file:")) { + return task.filePath.toLowerCase().includes(lowerQuery.replace("file:", "").trim()); + } else { + const titleMatch = task.title.toLowerCase().includes(lowerQuery); + const bodyMatch = task.body.join("\n").toLowerCase().includes(lowerQuery); + return titleMatch || bodyMatch; + } + }), + Completed: fileteredAllTasks.Completed.filter((task) => { + if (lowerQuery.startsWith("file:")) { + return task.filePath.toLowerCase().includes(lowerQuery.replace("file:", "").trim()); + } else { + const titleMatch = task.title.toLowerCase().includes(lowerQuery); + const bodyMatch = task.body.join("\n").toLowerCase().includes(lowerQuery); + return titleMatch || bodyMatch; + } + }) + }; + + setTimeout(() => { + plugin.settings.data.searchQuery = lowerQuery; + plugin.saveSettings(); + }, 100); + } + + return searchFilteredTasks; + } + + function handleFilterButtonClick(event: React.MouseEvent) { + try { + const currentBoardConfig = boardData; + if (Platform.isMobile || Platform.isMacOS) { + // If its a mobile platform, then we will open a modal instead of popover. + const filterModal = new AdvancedFilterModal( + plugin, false, boardData.id, currentBoardConfig!.name + ); + + // Set initial filter state + if (currentBoardConfig!.views[currentViewIndex].viewFilter) { + setTimeout(() => { + // Use type assertion to resolve non-null issues + // const filterState = filterModal.liveFilterState as RootFilterState; + if (filterModal.taskFilterComponent) { + filterModal.taskFilterComponent.loadFilterState(currentBoardConfig!.views[currentViewIndex].viewFilter); + } + }, 100); + } + + // Set the close callback - mainly used for handling cancel actions + filterModal.filterCloseCallback = async (filterState) => { + if (filterState) { + // Save the filter state to the board + let updatedcurrentBoardData = boardData; + updatedcurrentBoardData!.views[currentViewIndex].viewFilter = filterState; + setCurrentBoardData(updatedcurrentBoardData); + plugin.taskBoardFileManager.saveBoard(updatedcurrentBoardData); + + // Refresh the board view + eventEmitter.emit('REFRESH_BOARD'); + } + }; + + filterModal.open(); + + } else { + // If the platform is not mobile, then we will open a popover near the button. + + // Close existing popover if open + if (filterPopoverRef.current) { + filterPopoverRef.current.close(); + filterPopoverRef.current = null; + return; + } + + // Get button position + const buttonRect = event.currentTarget?.getBoundingClientRect(); + const position = { + x: buttonRect?.left ?? 100, + y: buttonRect?.bottom ?? 100 + }; + + // Create and show popover + const popover = new AdvancedFilterPopover( + plugin, + false, // forColumn = false since this is for board-level filter + boardData.id, + boardData?.name || "Board", + ); + + // Load existing filter state if available + if (currentBoardConfig!.views[currentViewIndex].viewFilter) { + // Wait for component to be created and loaded + setTimeout(() => { + if (popover.taskFilterComponent) { + popover.taskFilterComponent.loadFilterState(currentBoardConfig!.views[currentViewIndex].viewFilter!); + } + }, 100); + } + + // Set up close callback to save filter state + popover.onClose = async (filterState?: RootFilterState) => { + if (filterState) { + // Save the filter state to the board + const updatedcurrentBoardData = boardData; + updatedcurrentBoardData!.views[currentViewIndex].viewFilter = filterState; + setCurrentBoardData(updatedcurrentBoardData); + plugin.taskBoardFileManager.saveBoard(updatedcurrentBoardData); + + // Refresh the board view + eventEmitter.emit('REFRESH_BOARD'); + } + filterPopoverRef.current = null; + }; + + popover.showAtPosition(position); + filterPopoverRef.current = popover; + } + } catch (error) { + bugReporterManagerInsatance.showNotice(29, "Error showing filter popover", String(error), "TaskBoardViewContainer.tsx/handleFilterButtonClick"); + } + } + + function togglePropertyNameInSettings(propertyName: string) { + let visibleProperties = plugin.settings.data.visiblePropertiesList || []; + + if (visibleProperties.includes(propertyName)) { + visibleProperties.splice(visibleProperties.indexOf(propertyName), 1); + plugin.settings.data.visiblePropertiesList = visibleProperties; + + } else { + let index = -1; + switch (propertyName) { + case taskPropertiesNames.SubTasks: + index = visibleProperties.indexOf(taskPropertiesNames.SubTasksMinimized); + if (index > -1) + visibleProperties.splice(index, 1); + break; + case taskPropertiesNames.SubTasksMinimized: + index = visibleProperties.indexOf(taskPropertiesNames.SubTasks); + if (index > -1) + visibleProperties.splice(index, 1); + break; + case taskPropertiesNames.Description: + index = visibleProperties.indexOf(taskPropertiesNames.DescriptionMinimized); + if (index > -1) + visibleProperties.splice(index, 1); + break; + case taskPropertiesNames.DescriptionMinimized: + index = visibleProperties.indexOf(taskPropertiesNames.Description); + if (index > -1) + visibleProperties.splice(index, 1); + break; + } + visibleProperties.push(propertyName); + + plugin.settings.data.visiblePropertiesList = visibleProperties; + } + + plugin.saveSettings(); + eventEmitter.emit("REFRESH_BOARD"); + } + + function handlePropertiesBtnClick(event: React.MouseEvent) { + const propertyMenu = new Menu(); + + + propertyMenu.addItem((item) => { + item.setTitle(t("show-hide-properties")); + item.setIsLabel(true); + }); + propertyMenu.addSeparator(); + + propertyMenu.addItem((item) => { + item.setTitle(t("id")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.ID); + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.ID)) + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("checkbox")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Checkbox); + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Checkbox)) + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("status")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Status); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Status)) + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("priority")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Priority); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Priority)) + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("tags")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Tags); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Tags)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("time")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Time); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Time)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("reminder")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Reminder); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Reminder)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("created-date")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.CreatedDate); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.CreatedDate)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("start-date")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.StartDate); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.StartDate)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("scheduled-date")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.ScheduledDate); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.ScheduledDate)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("due-date")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.DueDate); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.DueDate)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("completed-date")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.CompletionDate); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.CompletionDate)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("cancelled-date")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.CancelledDate); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.CancelledDate)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("dependencies")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Dependencies); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Dependencies)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("file-name")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.FilePath); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.FilePath)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("file-name-in-header")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.FilePathInHeader); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.FilePathInHeader)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("parent-folder")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.ParentFolder); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.ParentFolder)) + }); + propertyMenu.addItem((item) => { + item.setTitle(t("full-path")); + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.FullPath); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.FullPath)) + }); + + propertyMenu.addSeparator(); + + propertyMenu.addItem((item) => { + item.setTitle(t("sub-tasks")); + const subTasksMenu = item.setSubmenu() + + subTasksMenu.addItem((item) => { + item.setTitle(t("visible")) + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.SubTasks); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)); + }); + + subTasksMenu.addItem((item) => { + item.setTitle(t("minimized")) + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.SubTasksMinimized); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasksMinimized)); + }); + + // subTasksMenu.addItem((item) => { + // item.setTitle(t("hidden")) + // item.onClick(async () => { + // togglePropertyNameInSettings(taskPropertiesNames.SubTasks); + // togglePropertyNameInSettings(taskPropertiesNames.SubTasksMinimized); + + // }) + // item.setChecked(!plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks) && !plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasksMinimized)); + // }); + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("description")); + const subTasksMenu = item.setSubmenu() + + subTasksMenu.addItem((item) => { + item.setTitle(t("visible")) + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.Description); + plugin.saveSettings(); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.Description)); + }); + + subTasksMenu.addItem((item) => { + item.setTitle(t("minimized")) + item.onClick(async () => { + togglePropertyNameInSettings(taskPropertiesNames.DescriptionMinimized); + plugin.saveSettings(); + + }) + item.setChecked(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.DescriptionMinimized)); + }); + }); + + // Use native event if available (React event has nativeEvent property) + propertyMenu.showAtMouseEvent( + (event instanceof MouseEvent ? event : event.nativeEvent) + ); + } + + function handleViewSelect(index: number) { + console.log("Will store the map view data first..."); + if (mapViewDataUpdated || viewPortDataOfMapViewUpdated.current) { + eventEmitter.emit("SAVE_MAP"); + // setMapViewDataUpdated(false); + // viewPortDataOfMapViewUpdated.current = false; + sleep(100); + } + + console.log("Now will switch the view..."); + if (index !== currentViewIndex) { + setSearchQuery(""); + plugin.settings.data.searchQuery = ""; + setCurrentViewIndex(index); + + // Update the board's lastViewId to persist view selection + if (boardData?.views && index >= 0 && index < boardData.views.length) { + let updatedBoard = { ...boardData }; + updatedBoard.lastViewId = boardData.views[index].viewId; + plugin.taskBoardFileManager.saveBoard(updatedBoard); + } + + // setTimeout(() => { + // eventEmitter.emit("REFRESH_BOARD"); + // // plugin.saveSettings(); + // }, 100); + } + // closeBoardSidebar(); // Close sidebar after selection + } + + function toggleBoardSidebar() { + if (boardDrawerVisible) { + closeBoardSidebar(); + } else { + openBoardSidebar(); + } + + setboardDrawerVisible(!boardDrawerVisible); + + // Update the board's lastViewId to persist view selection + if (boardData) { + let updatedBoardData = { ...boardData }; + updatedBoardData.viewsPanel.isOpen = !boardDrawerVisible; + plugin.taskBoardFileManager.debouncedSaveBoard(updatedBoardData); + } + } + + function openBoardSidebar() { + setboardDrawerVisible(true); + setSidebarAnimating(true); + } + + function closeBoardSidebar() { + setSidebarAnimating(false); + // Wait for animation to complete before hiding + setTimeout(() => { + setboardDrawerVisible(false); + }, 300); // Match animation duration + } + + function openHeaderMoreOptionsMenu(event: React.MouseEvent) { + const sortMenu = new Menu(); + + sortMenu.addItem((item) => { + item.setTitle(t("quick-actions")); + item.setIsLabel(true); + }); + sortMenu.addItem((item) => { + item.setTitle(t("refresh-the-board")); + item.setIcon("rotate-cw"); + item.onClick(async () => { + refreshBoardButton(); + }); + }); + sortMenu.addItem((item) => { + item.setTitle(t("show-hide-properties")); + item.setIcon("list"); + item.onClick(async () => { + handlePropertiesBtnClick(event); + }); + }); + sortMenu.addItem((item) => { + item.setTitle(t("open-board-filters-modal")); + item.setIcon(funnelIcon); + item.onClick(async () => { + handleFilterButtonClick(event); + }); + }); + sortMenu.addItem((item) => { + item.setTitle(t("open-board-configuration-modal")); + item.setIcon("settings"); + item.onClick(async () => { + openBoardConfigModal(plugin, boardData, currentViewIndex, (updatedBoard: Board) => { + // handleUpdateBoards(plugin, updatedBoards, setCurrentBoardData) + setCurrentBoardData(updatedBoard); + + setTimeout(() => { + eventEmitter.emit("REFRESH_BOARD"); + }, 100); + } + ) + }); + }); + sortMenu.addItem((item) => { + item.setTitle(t("scan-vault-modal")); + item.setIcon(ScanVaultIcon); + item.onClick(async () => { + openScanVaultModal(plugin); + }); + }); + + // Use native event if available (React event has nativeEvent property) + sortMenu.showAtMouseEvent( + (event instanceof MouseEvent ? event : event.nativeEvent) + ); + } + + function openBoardSidebarOption(event: React.MouseEvent) { + const propertyMenu = new Menu(); + + propertyMenu.addItem((item) => { + item.setTitle(t("show-details")); + item.setIsLabel(true); + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("description")); + item.setIcon(""); + item.setChecked(boardData.viewsPanel.propertiesToShow.contains(viewsPanelPropertiesToShow.Description)) + item.onClick(async () => { + if (boardData.viewsPanel.propertiesToShow.contains(viewsPanelPropertiesToShow.Description)) { + boardData.viewsPanel.propertiesToShow = boardData.viewsPanel.propertiesToShow.filter(prop => prop !== viewsPanelPropertiesToShow.Description); + } else { + boardData.viewsPanel.propertiesToShow.push(viewsPanelPropertiesToShow.Description); + } + setCurrentBoardData({ ...boardData }); + plugin.taskBoardFileManager.saveBoard(boardData); + setTimeout(() => { + eventEmitter.emit("REFRESH_BOARD"); + }, 100); + }); + }); + propertyMenu.addItem((item) => { + item.setTitle(t("progress")); + item.setIcon(""); + item.setChecked(boardData.viewsPanel.propertiesToShow.contains(viewsPanelPropertiesToShow.progress)) + item.onClick(async () => { + if (boardData.viewsPanel.propertiesToShow.contains(viewsPanelPropertiesToShow.progress)) { + boardData.viewsPanel.propertiesToShow = boardData.viewsPanel.propertiesToShow.filter(prop => prop !== viewsPanelPropertiesToShow.progress); + } else { + boardData.viewsPanel.propertiesToShow.push(viewsPanelPropertiesToShow.progress); + } + setCurrentBoardData({ ...boardData }); + plugin.taskBoardFileManager.saveBoard(boardData); + setTimeout(() => { + eventEmitter.emit("REFRESH_BOARD"); + }, 100); + }); + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("header-style")); + item.setIsLabel(true); + }); + + propertyMenu.addItem((item) => { + item.setTitle(t("buttons-belt")); + item.setIcon(""); + item.setChecked(boardData.viewsPanel.buttonsBelt) + item.onClick(async () => { + boardData.viewsPanel.buttonsBelt = !boardData.viewsPanel.buttonsBelt; + setCurrentBoardData({ ...boardData }); + plugin.taskBoardFileManager.saveBoard(boardData); + setTimeout(() => { + eventEmitter.emit("REFRESH_BOARD"); + }, 100); + }); + }); + propertyMenu.addItem((item) => { + item.setTitle(t("dropdown")); + item.setIcon(""); + item.setChecked(!boardData.viewsPanel.buttonsBelt) + item.onClick(async () => { + boardData.viewsPanel.buttonsBelt = !boardData.viewsPanel.buttonsBelt; + setCurrentBoardData({ ...boardData }); + plugin.taskBoardFileManager.saveBoard(boardData); + setTimeout(() => { + eventEmitter.emit("REFRESH_BOARD"); + }, 100); + }); + }); + + // Use native event if available (React event has nativeEvent property) + propertyMenu.showAtMouseEvent( + (event instanceof MouseEvent ? event : event.nativeEvent)); + } + + // Close sidebar when clicking outside or pressing escape + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape' && boardDrawerVisible) { + closeBoardSidebar(); + } + } + + if (boardDrawerVisible) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [boardDrawerVisible]); + + // Handle drawer resize + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing || !drawerRef.current) return; + + // Get the parent container's position + const parentRect = drawerRef.current.parentElement?.getBoundingClientRect(); + if (!parentRect) return; + + // Calculate new width based on mouse position + const newWidth = e.clientX - parentRect.left; + + // Set minimum and maximum width constraints (e.g., 150px to 500px) + const minWidth = 150; + const maxWidth = 500; + + if (newWidth >= minWidth && newWidth <= maxWidth) { + // Update the board data with the new width + const updatedBoard = { ...boardData }; + updatedBoard.viewsPanel.width = newWidth; + setCurrentBoardData(updatedBoard); + } + }; + + const handleMouseUp = () => { + if (isResizing) { + setIsResizing(false); + // Save the updated board configuration + if (boardData) { + plugin.taskBoardFileManager.debouncedSaveBoard(boardData); + } + } + }; + + if (isResizing) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + } + }, [isResizing, boardData, plugin]); + + const getViewTypeIconComponent = (viewTypeName: string | undefined, size: number) => { + let viewType = viewTypeName ?? boardData.views[currentViewIndex].viewType; + switch (viewType) { + case viewTypeNames.kanban: + return ; + case viewTypeNames.map: + return ; + default: + return ; + } + } + + // Update fade indicators on scroll + function updateScrollIndicators(element: HTMLElement) { + const { scrollLeft, scrollWidth, clientWidth } = element; + + element.classList.toggle('can-scroll-left', scrollLeft > 0); + element.classList.toggle('can-scroll-right', scrollLeft < scrollWidth - clientWidth - 1); + } + + /** + * Enable horizontal scrolling via vertical mouse wheel + * @param element - The scrollable container element + */ + function enableHorizontalWheelScroll(element: HTMLElement | null): () => void { + if (!element) return () => { }; + + const handleWheel = (event: WheelEvent) => { + // Only intercept vertical wheel events + if (event.deltaY === 0) return; + + // Prevent default vertical scroll + event.preventDefault(); + + // Convert vertical delta to horizontal scroll + // Multiply by 1.5 for slightly faster scrolling (adjust as needed) + element.scrollLeft += event.deltaY * 1.5; + }; + + // Use { passive: false } to allow preventDefault() + element.addEventListener('wheel', handleWheel, { passive: false }); + // Call this in your wheel handler + on mount/resize + element.addEventListener('scroll', () => updateScrollIndicators(element)); + updateScrollIndicators(element); // Initial check + + // Return cleanup function + return () => { + element.removeEventListener('wheel', handleWheel); + }; + } + const leftHeaderSecRef = useRef(null); + useEffect(() => { + const cleanup = enableHorizontalWheelScroll(leftHeaderSecRef.current); + return cleanup; // Cleanup on unmount + }, []); + + const viewDropdownRef = useRef(null); + const [showViewDropdown, setShowViewDropdown] = useState(false); + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (viewDropdownRef.current && !viewDropdownRef.current.contains(event.target as Node)) { + setShowViewDropdown(false); + } + } + + if (showViewDropdown) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [showViewDropdown]); + + // Display a message that no views are present inside this board. Stop here only instead of moving with the rest of the code which is dependent on the views. + if (!currentView) { + return ( +
+
+

{t("no-views-in-board")}

+ +
+
+ ); + } + + return ( +
+ {/* BOARD DRAWER TO LIST VIEW AND OTHER STUFF */} +
e.stopPropagation()} // Prevent closing when clicking inside sidebar + style={{ width: `${boardDrawerVisible ? boardData.viewsPanel.width : 0}px` }} + > +
+
+
+ +

{boardData.name}

+
+

{boardData.description}

+
+ +
+

{t("your-views")}

+
+ +
+
+ +
+ {boardData.views.length === 0 ? ( +
+ {t("no-views-created-yet")} +
+ ) : ( +
+ {boardData.views.map((view, index) => ( +
handleViewSelect(index)} + > +
+ {getViewTypeIconComponent(view.viewType, 20)} +
{view.viewName}
+
+ {boardData.viewsPanel.propertiesToShow.contains(viewsPanelPropertiesToShow.Description) && ( +
+ {view?.description} +
+ )} + {boardData.viewsPanel.propertiesToShow.contains(viewsPanelPropertiesToShow.progress) && ( +
+
+
+
+ + {(view.taskCount.completed)} / {view.taskCount.pending + view.taskCount?.completed} + +
+ )} +
+ ))} +
+ )} +
+
+
+ + {/* Resizable Separator */} + { + boardDrawerVisible && ( +
setIsResizing(true)} + aria-label={t("resize-drawer")} + /> + ) + } + + {/* MAIN VIEW CONTENT */} +
+
+
+ {boardData.views && boardData.views.length > 0 ? ( + <> + + {!boardDrawerVisible && boardData.viewsPanel.buttonsBelt ? ( +
+ {boardData.views.map((view, index) => ( + + ))} +
+ ) : ( +
+
{ + setShowViewDropdown(!showViewDropdown); + }} + > +
+ + {currentView.viewName} + + +
+ + {/* Custom Dropdown Menu */} +
+ {boardData.views.map((view, index) => ( +
handleViewSelect(index)} + > + {getViewTypeIconComponent(view.viewType, 20)} + {view.viewName} +
+ ))} +
+
+
+ )} + + ) : ( +
+ {!showSearchInput && ( + {t("no-view-created")} + )} +
+ )} +
+ +
+
+
+
+
+ + {(filteredTasks ? filteredTasks?.Completed.length : 0)} / {filteredTasks ? filteredTasks?.Pending.length + filteredTasks?.Completed.length : 0} + +
+ {showSearchInput && ( + setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSearchSubmit(); + } + }} + ref={input => { + if (input && showSearchInput) { + input.focus(); + } + }} + /> + )} + + + + + + + + + + + {/* */} + + {currentView && currentView.viewType === viewTypeNames.map && ( + + )} + + + + {(isMobileView || Platform.isMobile) && ( + + )} +
+
+ +
0 && !showAllElements ? 'taskBoardViewSectionWrapper--shifted' : ''}`}> +
+ {boardData && currentView ? ( + currentView.viewType === viewTypeNames.kanban ? ( + + ) : currentView.viewType === viewTypeNames.map ? ( + loading ? ( +
+ {freshInstall ? ( +

+ {t("fresh-install-1")} +
+
+ {t("fresh-install-2")} +
+
+ {t("fresh-install-3")} +

+ ) : ( + <> +
+

{t("loading-tasks")}

+ + )} +
+ ) : ( + + ) + ) : ( +
+ {/* Placeholder for other view types */} + {currentView.viewType === viewTypeNames.list && "List view coming soon."} + {currentView.viewType === viewTypeNames.table && "Table view coming soon."} + {currentView.viewType === viewTypeNames.inbox && "Inbox view coming soon."} + {currentView.viewType === viewTypeNames.gantt && "Gantt chart view coming soon."} +
+ ) + ) : ( +
+ {boardData && boardData.views?.length === 0 ? "No views available in this board." : "Select or create a new view to get started."} +
+ )} +
+
+
+
+ ); +}; + +export default TaskBoardViewContainer; diff --git a/src/components/TaskBoardViewContent.tsx b/src/components/TaskBoardViewContent.tsx deleted file mode 100644 index d59642b0..00000000 --- a/src/components/TaskBoardViewContent.tsx +++ /dev/null @@ -1,1048 +0,0 @@ -// src/components/TaskBoardViewContent.tsx - -import { Board, ColumnData, RootFilterState } from "../interfaces/BoardConfigs"; -import { CirclePlus, RefreshCcw, Search, SearchX, Filter, Menu as MenuICon, Settings, EllipsisVertical, List } from 'lucide-react'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { loadBoardsData, loadTasksAndMerge } from "src/utils/JsonFileOperations"; -import { taskItem, taskJsonMerged } from "src/interfaces/TaskItem"; - -import { App, debounce, Platform, Menu } from "obsidian"; -import type TaskBoard from "main"; -import { eventEmitter } from "src/services/EventEmitter"; -import { handleUpdateBoards } from "../utils/BoardOperations"; -import { bugReporter, openAddNewTaskModal, openBoardConfigModal, openScanVaultModal, openTaskBoardActionsModal } from "../services/OpenModals"; -import { columnSegregator } from 'src/utils/algorithms/ColumnSegregator'; -import { t } from "src/utils/lang/helper"; -import KanbanBoard from "./KanbanView/KanbanBoardView"; -import MapView from "./MapView/MapView"; -import { PENDING_SCAN_FILE_STACK, VIEW_TYPE_TASKBOARD } from "src/interfaces/Constants"; -import { ViewTaskFilterPopover } from "./BoardFilters/ViewTaskFilterPopover"; -import { boardFilterer } from "src/utils/algorithms/BoardFilterer"; -import { ViewTaskFilterModal } from 'src/components/BoardFilters'; -import { taskPropertiesNames, viewTypeNames } from "src/interfaces/Enums"; -import { ScanVaultIcon, funnelIcon } from "src/interfaces/Icons"; -import { bugReporterManagerInsatance } from "src/managers/BugReporter"; - -const TaskBoardViewContent: React.FC<{ app: App; plugin: TaskBoard; boardConfigs: Board[] }> = ({ app, plugin, boardConfigs }) => { - const [boards, setBoards] = useState(boardConfigs); - const [activeBoardIndex, setActiveBoardIndex] = useState(plugin.settings.data.globalSettings.lastViewHistory.boardIndex ?? 0); - const [allTasks, setAllTasks] = useState(); - const [filteredTasks, setFilteredTasks] = useState(null); - const [filteredTasksPerColumn, setFilteredTasksPerColumn] = useState([]); - const [viewType, setViewType] = useState(plugin.settings.data.globalSettings.lastViewHistory.viewedType || viewTypeNames.kanban); - - const [refreshCount, setRefreshCount] = useState(0); - const [loading, setLoading] = useState(true); - const [freshInstall, setFreshInstall] = useState(false); - const [showSearchInput, setShowSearchInput] = useState(plugin.settings.data.globalSettings.searchQuery ? true : false); - const [searchQuery, setSearchQuery] = useState(plugin.settings.data.globalSettings.searchQuery ?? ""); - - const filterPopoverRef = useRef(null); - - const [showAllElements, setShowAllElements] = useState(true); - const [leafWidth, setLeafWidth] = useState(1000); - const [isMobileView, setIsMobileView] = useState(false); - const [showBoardSidebar, setShowBoardSidebar] = useState(false); - const [sidebarAnimating, setSidebarAnimating] = useState(false); - const [editorModified, setEditorModified] = useState(plugin.editorModified); - - // plugin.registerEvent( - // plugin.app.workspace.on("resize", () => { - // // Now I should find if the leaf of type taskboard-view is active or not. If its active then I should find its width. If its less than 400px then hide the progress bar. - // const taskBoardLeaf = - // plugin.app.workspace.getLeavesOfType(VIEW_TYPE_TASKBOARD)[0]; - // console.log( - // "Window resized", - // "\nTaskBoardLeaf:", - // taskBoardLeaf - // ); - // if (taskBoardLeaf) { - // const width = taskBoardLeaf.width; - // console.log("TaskBoardLeaf width:", width); - // setLeafWidth(width); - // } - // }) - // ); - - useEffect(() => { - const handleResize = () => { - const taskBoardLeaf = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_TASKBOARD)[0]; - if (taskBoardLeaf) { - setLeafWidth(taskBoardLeaf.width); - } - }; - handleResize(); - plugin.registerEvent(plugin.app.workspace.on("resize", handleResize)); - return () => { - // cleanup if needed - }; - }, []); - - useEffect(() => { - setShowAllElements(leafWidth >= 1000); - setIsMobileView(leafWidth <= 800); // For even little bigger screen smartphones, let go with 800 - }, [leafWidth]); - - useEffect(() => { - const fetchData = async () => { - try { - const data = await loadBoardsData(plugin); - setBoards(data); - - const allTasks = await loadTasksAndMerge(plugin, true); - if (allTasks) { - setAllTasks(allTasks); - setFreshInstall(false); - } - } catch (error) { - console.error( - "Error loading tasks cache from disk\nIf this is appearing on a fresh install then no need to worry.\n", - error - ); - setFreshInstall(true); - } - }; - - fetchData(); - }, [refreshCount]); - - const allTasksArrangedPerColumn = useMemo(() => { - // console.log("Calculating allTasksArrangedPerColumn..."); - setFilteredTasksPerColumn([]); - if (allTasks && boards[activeBoardIndex]) { - // Apply board filters to pending tasks - const currentBoard = boards[activeBoardIndex]; - const boardFilter = currentBoard.boardFilter; - - // Create a copy of allTasks with filtered pending tasks - const filteredAllTasks = { - ...allTasks, - Pending: boardFilterer(allTasks.Pending, boardFilter), - Completed: boardFilterer(allTasks.Completed, boardFilter), - }; - plugin.settings.data.boardConfigs[activeBoardIndex].taskCount = { - pending: filteredAllTasks.Pending.length, - completed: filteredAllTasks.Completed.length, - }; - setFilteredTasks(filteredAllTasks); - - if (searchQuery.trim() !== "") { - const searchQueryFilteredTasks = handleSearchSubmit(filteredAllTasks); - return currentBoard.columns - .filter((column) => column.active) - .map((column: ColumnData) => - columnSegregator(plugin.settings, activeBoardIndex, column, searchQueryFilteredTasks) - ); - } else { - return currentBoard.columns - .filter((column) => column.active) - .map((column: ColumnData) => - columnSegregator(plugin.settings, activeBoardIndex, column, filteredAllTasks, (updatedBoardData: Board) => { - // I think this below code is not required as we simply want to update the data on the disk. - // setBoards((prevBoards) => { - // const updatedBoards = [...prevBoards]; - // updatedBoards[activeBoardIndex] = updatedBoardData; - // return updatedBoards; - // }); - - plugin.settings.data.boardConfigs[activeBoardIndex] = updatedBoardData; - // Technically, at later point in time, when user will make any changes, the latest data will be updated on the disk, so we need not have to update it everytime during this column seggregation. - // const newSettings = plugin.settings; - // plugin.saveSettings(newSettings); - }) - ); - } - - } - return []; - }, [allTasks, activeBoardIndex]); - - useEffect(() => { - if (allTasksArrangedPerColumn.length > 0) { - setLoading(false); - } - }, [allTasksArrangedPerColumn]); - - const debouncedRefreshColumn = useCallback( - debounce(async () => { - try { - const allTasks = await loadTasksAndMerge(plugin, false); - setAllTasks(allTasks); - } catch (error) { - bugReporterManagerInsatance.showNotice(28, "Error loading tasks on column refresh", String(error), "TaskBoardViewContent.tsx/debouncedRefreshColumn"); - } - }, 500), - [plugin] - ); - - useEffect(() => { - eventEmitter.on("REFRESH_COLUMN", debouncedRefreshColumn); - return () => eventEmitter.off("REFRESH_COLUMN", debouncedRefreshColumn); - }, [debouncedRefreshColumn]); - - useEffect(() => { - const refreshBoardListener = () => setRefreshCount((prev) => prev + 1); - eventEmitter.on("REFRESH_BOARD", refreshBoardListener); - return () => eventEmitter.off("REFRESH_BOARD", refreshBoardListener); - }, []); - - useEffect(() => { - const refreshView = (viewType: string) => { - setViewType(viewType); - plugin.settings.data.globalSettings.lastViewHistory.viewedType = viewType; - plugin.saveSettings(); - }; - eventEmitter.on("SWITCH_VIEW", refreshView); - return () => eventEmitter.off("SWITCH_VIEW", refreshView); - }, []); - - // Listen to editor modified state changes - useEffect(() => { - const handleEditorModifiedChange = (modified: boolean) => { - setEditorModified(modified); - }; - - eventEmitter.on("EDITOR_MODIFIED_CHANGED", handleEditorModifiedChange); - return () => eventEmitter.off("EDITOR_MODIFIED_CHANGED", handleEditorModifiedChange); - }, []); - - const refreshBoardButton = useCallback(() => { - plugin.realTimeScanner.processAllUpdatedFiles().then(() => console.log("Finished processing all updated files.")); - plugin.processCreateQueue().then(() => console.log("Finished processing create queue.")); - plugin.processDeleteQueue().then(() => console.log("Finished processing delete queue.")); - plugin.processRenameQueue().then(() => console.log("Finished processing rename queue.")); - - setTimeout(() => { - console.log("Now will emit REFRESH_BOARD event..."); - eventEmitter.emit("REFRESH_BOARD"); - }, 100) - }, []); - - function handleOpenAddNewTaskModal() { - openAddNewTaskModal(app, plugin); - } - - function handleOpenTaskBoardActionsModal() { - openTaskBoardActionsModal(plugin, activeBoardIndex); - } - - function handleSearchButtonClick() { - if (showSearchInput) { - setSearchQuery(""); - // el.currentTarget.focus(); - setFilteredTasksPerColumn([]); - plugin.settings.data.globalSettings.searchQuery = ""; - - eventEmitter.emit("REFRESH_COLUMN"); - plugin.saveSettings(); - setShowSearchInput(false); - } else { - setSearchQuery(plugin.settings.data.globalSettings.searchQuery || ""); - handleSearchSubmit(); - setShowSearchInput(true); - } - } - - // function highlightMatch(text: string, query: string): string { - // const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // const regex = new RegExp(`(${escapedQuery})`, "gi"); - // return text.replace(regex, `$1`); - // } - - function handleSearchSubmit(fileteredAllTasks?: taskJsonMerged): taskJsonMerged | null { - if (!searchQuery.trim()) { - setFilteredTasksPerColumn([]); - return null; - } - - // plugin.settings.data.globalSettings.searchQuery = searchQuery; - - const lowerQuery = searchQuery.toLowerCase(); - let searchFilteredTasks: taskJsonMerged | null = null; - - if (fileteredAllTasks) { - searchFilteredTasks = { - Pending: fileteredAllTasks.Pending.filter((task) => { - if (lowerQuery.startsWith("file:")) { - return task.filePath.toLowerCase().includes(lowerQuery.replace("file:", "").trim()); - } else { - const titleMatch = task.title.toLowerCase().includes(lowerQuery); - const bodyMatch = task.body.join("\n").toLowerCase().includes(lowerQuery); - return titleMatch || bodyMatch; - } - }), - Completed: fileteredAllTasks.Completed.filter((task) => { - if (lowerQuery.startsWith("file:")) { - return task.filePath.toLowerCase().includes(lowerQuery.replace("file:", "").trim()); - } else { - const titleMatch = task.title.toLowerCase().includes(lowerQuery); - const bodyMatch = task.body.join("\n").toLowerCase().includes(lowerQuery); - return titleMatch || bodyMatch; - } - }) - }; - } - else { - const filtered = allTasksArrangedPerColumn.map((column) => { - const filteredTasks = column - .filter((task) => { - if (lowerQuery.startsWith("file:")) { - return task.filePath.toLowerCase().includes(lowerQuery.replace("file:", "").trim()); - } else { - const titleMatch = task.title.toLowerCase().includes(lowerQuery); - const bodyMatch = task.body.join("\n").toLowerCase().includes(lowerQuery); - return titleMatch || bodyMatch; - } - }); - // TODO : This highliting option also cannot work as it destroys the other functionalities of the taskItem. - // .map((task) => { - // const highlightedTitle = highlightMatch(task.title, searchQuery); - // const highlightedBody = highlightMatch(task.body.join("\n"), searchQuery); - - // return { - // ...task, - // title: highlightedTitle, - // body: highlightedBody.split("\n"), - // }; - // }); - return filteredTasks; - }); - setFilteredTasksPerColumn(filtered); - - setTimeout(() => { - plugin.settings.data.globalSettings.searchQuery = lowerQuery; - plugin.saveSettings(); - }, 100); - } - - - return searchFilteredTasks; - } - - function handleViewTypeChange(e: React.ChangeEvent) { - const newViewType = e.target.value; - setViewType(newViewType); - plugin.settings.data.globalSettings.lastViewHistory.viewedType = newViewType; - plugin.saveSettings(); - } - - function handleFilterButtonClick(event: React.MouseEvent) { - try { - const currentBoardConfig = boards[activeBoardIndex]; - if (Platform.isMobile || Platform.isMacOS) { - // If its a mobile platform, then we will open a modal instead of popover. - const filterModal = new ViewTaskFilterModal( - plugin, false, undefined, activeBoardIndex, currentBoardConfig.name - ); - - // Set initial filter state - if (currentBoardConfig.boardFilter) { - setTimeout(() => { - // Use type assertion to resolve non-null issues - // const filterState = filterModal.liveFilterState as RootFilterState; - if (filterModal.taskFilterComponent) { - filterModal.taskFilterComponent.loadFilterState(currentBoardConfig.boardFilter!); - } - }, 100); - } - - // Set the close callback - mainly used for handling cancel actions - filterModal.filterCloseCallback = async (filterState) => { - if (filterState) { - // Save the filter state to the board - const updatedBoards = [...boards]; - updatedBoards[activeBoardIndex].boardFilter = filterState; - setBoards(updatedBoards); - - // Persist to settings - plugin.settings.data.boardConfigs[activeBoardIndex].boardFilter = filterState; - await plugin.saveSettings(); - - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } - }; - - filterModal.open(); - - } else { - // If the platform is not mobile, then we will open a popover near the button. - - // Close existing popover if open - if (filterPopoverRef.current) { - filterPopoverRef.current.close(); - filterPopoverRef.current = null; - return; - } - - // Get button position - const buttonRect = event.currentTarget?.getBoundingClientRect(); - const position = { - x: buttonRect?.left ?? 100, - y: buttonRect?.bottom ?? 100 - }; - - // Create and show popover - const popover = new ViewTaskFilterPopover( - plugin, - false, // forColumn = false since this is for board-level filter - undefined, - activeBoardIndex, - boards[activeBoardIndex]?.name || "Board", - ); - - // Load existing filter state if available - if (currentBoardConfig.boardFilter) { - // Wait for component to be created and loaded - setTimeout(() => { - if (popover.taskFilterComponent) { - popover.taskFilterComponent.loadFilterState(currentBoardConfig.boardFilter!); - } - }, 100); - } - - // Set up close callback to save filter state - popover.onClose = async (filterState?: RootFilterState) => { - if (filterState) { - // Save the filter state to the board - const updatedBoards = [...boards]; - updatedBoards[activeBoardIndex].boardFilter = filterState; - setBoards(updatedBoards); - - // Persist to settings - plugin.settings.data.boardConfigs[activeBoardIndex].boardFilter = filterState; - await plugin.saveSettings(); - - // Refresh the board view - eventEmitter.emit('REFRESH_BOARD'); - } - filterPopoverRef.current = null; - }; - - popover.showAtPosition(position); - filterPopoverRef.current = popover; - } - } catch (error) { - bugReporterManagerInsatance.showNotice(29, "Error showing filter popover", String(error), "TaskBoardViewContent.tsx/handleFilterButtonClick"); - } - } - - function togglePropertyNameInSettings(propertyName: string) { - let visibleProperties = plugin.settings.data.globalSettings.visiblePropertiesList || []; - - console.log("Current properties list :", visibleProperties, "\nRemove following property :", propertyName, "\nWill remove from the following index :", visibleProperties.indexOf(propertyName)); - if (visibleProperties.includes(propertyName)) { - visibleProperties.splice(visibleProperties.indexOf(propertyName), 1); - plugin.settings.data.globalSettings.visiblePropertiesList = visibleProperties; - - } else { - console.log("Property Name:", propertyName); - let index = -1; - switch (propertyName) { - case taskPropertiesNames.SubTasks: - index = visibleProperties.indexOf(taskPropertiesNames.SubTasksMinimized); - if (index > -1) - visibleProperties.splice(index, 1); - break; - case taskPropertiesNames.SubTasksMinimized: - index = visibleProperties.indexOf(taskPropertiesNames.SubTasks); - if (index > -1) - visibleProperties.splice(index, 1); - break; - case taskPropertiesNames.Description: - index = visibleProperties.indexOf(taskPropertiesNames.DescriptionMinimized); - if (index > -1) - visibleProperties.splice(index, 1); - break; - case taskPropertiesNames.DescriptionMinimized: - index = visibleProperties.indexOf(taskPropertiesNames.Description); - if (index > -1) - visibleProperties.splice(index, 1); - break; - } - visibleProperties.push(propertyName); - - plugin.settings.data.globalSettings.visiblePropertiesList = visibleProperties; - } - - plugin.saveSettings(); - eventEmitter.emit("REFRESH_BOARD"); - } - - function handlePropertiesBtnClick(event: React.MouseEvent) { - const propertyMenu = new Menu(); - - - propertyMenu.addItem((item) => { - item.setTitle(t("show-hide-properties")); - item.setIsLabel(true); - }); - propertyMenu.addSeparator(); - - propertyMenu.addItem((item) => { - item.setTitle(t("id")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.ID); - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ID)) - }); - - propertyMenu.addItem((item) => { - item.setTitle(t("checkbox")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Checkbox); - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Checkbox)) - }); - - propertyMenu.addItem((item) => { - item.setTitle(t("status")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Status); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Status)) - }); - - propertyMenu.addItem((item) => { - item.setTitle(t("priority")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Priority); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Priority)) - }); - - propertyMenu.addItem((item) => { - item.setTitle(t("tags")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Tags); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Tags)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("time")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Time); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Time)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("reminder")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Reminder); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Reminder)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("created-date")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.CreatedDate); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.CreatedDate)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("start-date")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.StartDate); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.StartDate)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("scheduled-date")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.ScheduledDate); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ScheduledDate)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("due-date")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.DueDate); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.DueDate)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("completed-date")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.CompletionDate); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.CompletionDate)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("cancelled-date")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.CancelledDate); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.CancelledDate)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("dependencies")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Dependencies); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Dependencies)) - }); - propertyMenu.addItem((item) => { - item.setTitle(t("file-name")); - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.FilePath); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.FilePath)) - }); - - propertyMenu.addSeparator(); - - propertyMenu.addItem((item) => { - item.setTitle(t("sub-tasks")); - const subTasksMenu = item.setSubmenu() - - subTasksMenu.addItem((item) => { - item.setTitle(t("visible")) - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.SubTasks); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)); - }); - - subTasksMenu.addItem((item) => { - item.setTitle(t("minimized")) - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.SubTasksMinimized); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasksMinimized)); - }); - - // subTasksMenu.addItem((item) => { - // item.setTitle(t("hidden")) - // item.onClick(async () => { - // togglePropertyNameInSettings(taskPropertiesNames.SubTasks); - // togglePropertyNameInSettings(taskPropertiesNames.SubTasksMinimized); - - // }) - // item.setChecked(!plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks) && !plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasksMinimized)); - // }); - }); - - propertyMenu.addItem((item) => { - item.setTitle(t("description")); - const subTasksMenu = item.setSubmenu() - - subTasksMenu.addItem((item) => { - item.setTitle(t("visible")) - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.Description); - plugin.saveSettings(); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Description)); - }); - - subTasksMenu.addItem((item) => { - item.setTitle(t("minimized")) - item.onClick(async () => { - togglePropertyNameInSettings(taskPropertiesNames.DescriptionMinimized); - plugin.saveSettings(); - - }) - item.setChecked(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.DescriptionMinimized)); - }); - }); - - // Use native event if available (React event has nativeEvent property) - propertyMenu.showAtMouseEvent( - (event instanceof MouseEvent ? event : event.nativeEvent) - ); - } - - function handleBoardSelection(index: number) { - if (index !== activeBoardIndex) { - setSearchQuery(""); - setFilteredTasksPerColumn([]); - plugin.settings.data.globalSettings.searchQuery = ""; - plugin.settings.data.globalSettings.lastViewHistory.boardIndex = index; - setActiveBoardIndex(index); - setTimeout(() => { - eventEmitter.emit("REFRESH_BOARD"); - plugin.saveSettings(); - }, 100); - - } - closeBoardSidebar(); // Close sidebar after selection - } - - function toggleBoardSidebar() { - if (showBoardSidebar) { - closeBoardSidebar(); - } else { - openBoardSidebar(); - } - } - - function openBoardSidebar() { - setShowBoardSidebar(true); - setSidebarAnimating(true); - } - - function closeBoardSidebar() { - setSidebarAnimating(false); - // Wait for animation to complete before hiding - setTimeout(() => { - setShowBoardSidebar(false); - }, 300); // Match animation duration - } - - function openHeaderMoreOptionsMenu(event: React.MouseEvent) { - const sortMenu = new Menu(); - - sortMenu.addItem((item) => { - item.setTitle(t("quick-actions")); - item.setIsLabel(true); - }); - sortMenu.addItem((item) => { - item.setTitle(t("refresh-the-board")); - item.setIcon("rotate-cw"); - item.onClick(async () => { - refreshBoardButton(); - }); - }); - sortMenu.addItem((item) => { - item.setTitle(t("show-hide-properties")); - item.setIcon("list"); - item.onClick(async () => { - handlePropertiesBtnClick(event); - }); - }); - sortMenu.addItem((item) => { - item.setTitle(t("open-board-filters-modal")); - item.setIcon(funnelIcon); - item.onClick(async () => { - handleFilterButtonClick(event); - }); - }); - sortMenu.addItem((item) => { - item.setTitle(t("open-board-configuration-modal")); - item.setIcon("settings"); - item.onClick(async () => { - openBoardConfigModal(plugin, boards, activeBoardIndex, (updatedBoards) => - handleUpdateBoards(plugin, updatedBoards, setBoards) - ); - }); - }); - sortMenu.addItem((item) => { - item.setTitle(t("scan-vault-modal")); - item.setIcon(ScanVaultIcon); - item.onClick(async () => { - openScanVaultModal(plugin.app, plugin); - }); - }); - - - sortMenu.addItem((item) => { - item.setTitle(t("view-type")); - item.setIsLabel(true); - }); - sortMenu.addItem((item) => { - item.setTitle(t("kanban-view")); - item.setIcon("square-kanban"); - item.onClick(async () => { - eventEmitter.emit("SWITCH_VIEW", 'kanban'); - }); - }); - sortMenu.addItem((item) => { - item.setTitle(t("map-view")); - item.setIcon("waypoints"); - item.onClick(async () => { - eventEmitter.emit("SWITCH_VIEW", 'map'); - }); - }); - - // Use native event if available (React event has nativeEvent property) - sortMenu.showAtMouseEvent( - (event instanceof MouseEvent ? event : event.nativeEvent) - ); - } - - // useEffect(() => { - // const taskBoardLeaf = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_TASKBOARD)[0]; - // if (taskBoardLeaf) { - // console.log("View width :", taskBoardLeaf.width); - // } - // }, [leafWidth]); - - // Close sidebar when clicking outside or pressing escape - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Escape' && showBoardSidebar) { - closeBoardSidebar(); - } - } - - if (showBoardSidebar) { - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - } - }, [showBoardSidebar]); - - return ( -
-
- {!showAllElements ? ( - // Mobile view: Hamburger button + current board name -
- - {!showSearchInput && ( - {boards[activeBoardIndex]?.name} - )} -
- ) : ( - // Desktop view: Original board titles -
- {boards.map((board, index) => ( - - ))} -
- )} -
-
-
= 1500 ? "" : "-hidden"}`}> -
-
- - {(filteredTasks ? filteredTasks?.Completed.length : 0)} / {filteredTasks ? filteredTasks?.Pending.length + filteredTasks?.Completed.length : 0} - -
- {showSearchInput && ( - setSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleSearchSubmit(); - } - }} - ref={input => { - if (input && showSearchInput) { - input.focus(); - } - }} - /> - )} - - - - - - - - - - - {/* */} - - - - - - {(isMobileView || Platform.isMobile) && ( - - )} -
-
- - {/* Mobile board sidebar overlay */} - {!showAllElements && showBoardSidebar && ( -
-
e.stopPropagation()} // Prevent closing when clicking inside sidebar - > -
-

{t("your-boards")}

-
-
-
- {boards.map((board, index) => ( -
handleBoardSelection(index)} - > -
- {board.name} -
-
- {board?.description} -
-
-
-
-
- - {(board?.taskCount ? board.taskCount.completed : 0)} / {board?.taskCount ? board?.taskCount.pending + board?.taskCount?.completed : 0} - -
-
- ))} -
-
- -
-
-
-
- ) - } - -
- {boards[activeBoardIndex] ? ( - viewType === viewTypeNames.kanban ? ( - 0 ? filteredTasksPerColumn : allTasksArrangedPerColumn} - loading={loading} - freshInstall={freshInstall} - /> - ) : viewType === viewTypeNames.map ? ( - loading ? ( -
- {freshInstall ? ( -

- {t("fresh-install-1")} -
-
- {t("fresh-install-2")} -
-
- {t("fresh-install-3")} -

- ) : ( - <> -
-

{t("loading-tasks")}

- - )} -
- ) : ( - 0 ? filteredTasksPerColumn : allTasksArrangedPerColumn} - focusOnTaskId={plugin.settings.data.globalSettings.lastViewHistory.taskId || ""} - /> - ) - ) : ( -
- {/* Placeholder for other view types */} - {viewType === "list" && "List view coming soon."} - {viewType === "table" && "Table view coming soon."} - {viewType === "calender" && "Calender view coming soon."} - {viewType === "gantt" && "Gantt chart view coming soon."} -
- ) - ) : ( -
- Switch to different board. -
- )} -
-
- ); -}; - -export default TaskBoardViewContent; diff --git a/src/components/KanbanView/TaskItem.tsx b/src/components/TaskCard/TaskItem.tsx similarity index 63% rename from src/components/KanbanView/TaskItem.tsx rename to src/components/TaskCard/TaskItem.tsx index ed5f5e82..60a9a995 100644 --- a/src/components/KanbanView/TaskItem.tsx +++ b/src/components/TaskCard/TaskItem.tsx @@ -2,78 +2,63 @@ import { FaEdit, FaTrash } from 'react-icons/fa'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { checkboxStateSwitcher, extractCheckboxSymbol, getObsidianIndentationSetting, isTaskCompleted, isTaskLine } from 'src/utils/CheckBoxUtils'; -import { handleCheckboxChange, handleDeleteTask, handleSubTasksChange } from 'src/utils/taskLine/TaskItemEventHandlers'; -import { hookMarkdownLinkMouseEventHandlers, markdownButtonHoverPreviewEvent } from 'src/services/MarkdownHoverPreview'; - -import { Component, Notice, Platform, Menu, TFile } from 'obsidian'; -import { MarkdownUIRenderer } from 'src/services/MarkdownUIRenderer'; -import { cleanTaskTitleLegacy } from 'src/utils/taskLine/TaskContentFormatter'; -import { updateRGBAOpacity } from 'src/utils/UIHelpers'; -import { t } from 'src/utils/lang/helper'; -import TaskBoard from 'main'; -import { Board } from 'src/interfaces/BoardConfigs'; -import { TaskRegularExpressions, TASKS_PLUGIN_DEFAULT_SYMBOLS } from 'src/regularExpressions/TasksPluginRegularExpr'; -import { getStatusNameFromStatusSymbol, isTaskNotePresentInTags } from 'src/utils/taskNote/TaskNoteUtils'; -import { allowedFileExtensionsRegEx } from 'src/regularExpressions/MiscelleneousRegExpr'; -import { bugReporter } from 'src/services/OpenModals'; +import { Component, Notice, Platform, Menu, TFile, MenuItem } from 'obsidian'; import { ChevronDown, EllipsisVertical, Grip } from 'lucide-react'; -import { EditButtonMode, viewTypeNames, colTypeNames, taskPropertiesNames } from 'src/interfaces/Enums'; -import { getCustomStatusOptionsForDropdown, priorityEmojis } from 'src/interfaces/Mapping'; -import { taskItem, UpdateTaskEventData } from 'src/interfaces/TaskItem'; -import { matchTagsWithWildcards, verifySubtasksAndChildtasksAreComplete } from 'src/utils/algorithms/ScanningFilterer'; -import { handleTaskNoteStatusChange, handleTaskNoteBodyChange } from 'src/utils/taskNote/TaskNoteEventHandlers'; -import { eventEmitter } from 'src/services/EventEmitter'; -import { RxDragHandleDots2 } from 'react-icons/rx'; -import { getUniversalDateEmoji, getUniversalDateFromTask, parseUniversalDate } from 'src/utils/DateTimeCalculations'; -import { getTaskFromId } from 'src/utils/TaskItemUtils'; -import { handleEditTask, updateTaskItemStatus, updateTaskItemPriority, updateTaskItemDate, updateTaskItemReminder, updateTaskItemTags } from 'src/utils/UserTaskEvents'; -import EditTagsModal from 'src/modals/EditTagsModal'; - -// Helper modal functions may be provided elsewhere; declare them for TypeScript -declare function showTextInputModal(app: any, options: { title?: string; placeholder?: string; initialValue?: string }): Promise; -declare function showConfirmationModal(app: any, options: any): Promise; -import { dragDropTasksManagerInsatance, currentDragDataPayload } from 'src/managers/DragDropTasksManager'; -import { bugReporterManagerInsatance } from 'src/managers/BugReporter'; +import { isToday, isBefore, isAfter, startOfDay, compareAsc } from 'date-fns'; +import { DEFAULT_DATE_FORMAT } from '../../interfaces/Constants.js'; +import { taskPropertiesNames, TagColorType, EditButtonMode, UniversalDateOptions, viewTypeNames, colTypeNames } from '../../interfaces/Enums.js'; +import { getCustomStatusOptionsForDropdown, getPriorityOptionsForDropdown, priorityEmojis } from '../../interfaces/Mapping.js'; +import { UpdateTaskEventData, taskItem } from '../../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +import { currentDragDataPayload, dragDropTasksManagerInsatance } from '../../managers/DragDropTasksManager.js'; +import { DateTimePickerModal } from '../../modals/date_time_picker/DateTimePickerModal.js'; +import { showTextInputModal } from '../../modals/TextInputModal.js'; +import { TaskRegularExpressions, TASKS_PLUGIN_DEFAULT_SYMBOLS } from '../../regularExpressions/TasksPluginRegularExpr.js'; +import { eventEmitter } from '../../services/EventEmitter.js'; +import { hookMarkdownLinkMouseEventHandlers, markdownButtonHoverPreviewEvent } from '../../services/MarkdownHoverPreview.js'; +import { MarkdownUIRenderer } from '../../services/MarkdownUIRenderer.js'; +import { openDateInputModal } from '../../services/OpenModals.js'; +import { matchTagsWithWildcards, verifySubtasksAndChildtasksAreComplete } from '../../utils/algorithms/ScanningFilterer.js'; +import { isTaskCompleted, isTaskLine, extractCheckboxSymbol, checkboxStateSwitcher, getObsidianIndentationSetting } from '../../utils/CheckBoxUtils.js'; +import { getUniversalDateFromTask, robustDateParser } from '../../utils/DateTimeCalculations.js'; +import { getAllTaskTags, getTaskFromId } from '../../utils/TaskItemUtils.js'; +import { cleanTaskTitleLegacy } from '../../utils/taskLine/TaskContentFormatter.js'; +import { handleCheckboxChange, handleDeleteTask, handleSubTasksChange } from '../../utils/taskLine/TaskItemEventHandlers.js'; +import { handleTaskNoteStatusChange, handleTaskNoteBodyChange } from '../../utils/taskNote/TaskNoteEventHandlers.js'; +import { isTaskNotePresentInTags, getStatusNameFromStatusSymbol } from '../../utils/taskNote/TaskNoteUtils.js'; +import { updateRGBAOpacity } from '../../utils/UIHelpers.js'; +import { handleEditTask, updateTaskItemStatus, updateTaskItemPriority, updateTaskItemDate, updateTaskItemReminder } from '../../utils/UserTaskEvents.js'; +import { TaskCardProps } from './TaskItemV2.js'; +import { t } from '../../utils/lang/helper.js'; export interface swimlaneDataProp { property: string; value: string; } -export interface TaskCardComponentProps { - dataAttributeIndex: number; - plugin: TaskBoard; - task: taskItem; - activeBoardSettings: Board; - columnIndex?: number; - swimlaneData?: swimlaneDataProp; -} - -const TaskItem: React.FC = ({ dataAttributeIndex, plugin, task, activeBoardSettings, columnIndex, swimlaneData }) => { - const globalSettings = plugin.settings.data.globalSettings; - const taskNoteIdentifierTag = plugin.settings.data.globalSettings.taskNoteIdentifierTag; +const TaskItem: React.FC = ({ dataAttributeIndex, plugin, task, activeBoardID, activeViewIndex, activeViewType, kanbanViewData, columnIndex, swimlaneData }) => { + const globalSettings = plugin.settings.data; + const taskNoteIdentifierTag = plugin.settings.data.taskNoteIdentifierTag; const isTaskNote = isTaskNotePresentInTags(taskNoteIdentifierTag, task.tags); const isThistaskCompleted = isTaskNote ? isTaskCompleted(task.status, true, plugin.settings) : isTaskCompleted(task.title, false, plugin.settings) - const columnData = columnIndex !== undefined ? activeBoardSettings?.columns[columnIndex - 1] : undefined; + const columnData = columnIndex !== undefined && kanbanViewData ? kanbanViewData.columns[columnIndex - 1] : undefined; const showDescriptionSection = globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Description) ?? true; - const [isDragging, setIsDragging] = useState(false); const [isChecked, setIsChecked] = useState(isThistaskCompleted); const [cardLoadingAnimation, setCardLoadingAnimation] = useState(false); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); - const [showSubtasks, setShowSubtasks] = useState(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)); + const [showSubtasks, setShowSubtasks] = useState(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)); useEffect(() => { - if (plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)) { + if (plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)) { setShowSubtasks(true); } else { setShowSubtasks(false); } - }, [plugin.settings.data.globalSettings]); + }, [plugin.settings.data]); - const [universalDate, setUniversalDate] = useState(() => getUniversalDateFromTask(task, plugin)); + const [universalDate, setUniversalDate] = useState(() => getUniversalDateFromTask(task, globalSettings.universalDate)); useEffect(() => { - setUniversalDate(getUniversalDateFromTask(task, plugin)); + setUniversalDate(getUniversalDateFromTask(task, globalSettings.universalDate)); }, [task.due, task.startDate, task.scheduledDate]); @@ -115,8 +100,8 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin // if (titleElement && task.title !== "") { // let cleanedTitle = cleanTaskTitleLegacy(task); - // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContent.tsx - // // const searchQuery = plugin.settings.data.globalSettings.searchQuery || ''; + // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContainer.tsx + // // const searchQuery = plugin.settings.data.searchQuery || ''; // // if (searchQuery) { // // const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // // const regex = new RegExp(`(${escapedQuery})`, "gi"); @@ -156,7 +141,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin el.innerHTML = ''; if (task.title === "") return; - const cleanedTitle = cleanTaskTitleLegacy(task); + const cleanedTitle = isTaskNote ? task.title : cleanTaskTitleLegacy(task); await MarkdownUIRenderer.renderTaskDisc( plugin.app, @@ -173,14 +158,18 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin hookMarkdownLinkMouseEventHandlers(plugin.app, plugin, el, task.filePath, task.filePath); } catch (err) { - console.error('Error rendering task title:', err); + bugReporterManagerInsatance.addToLogs( + 122, + String(err), + "TaskItem.tsx/Main title rendering useEffect", + ); } })(); // return () => { // cancelled = true; // }; - }, [task.id, task.title, task.filePath, plugin.settings.data.globalSettings.searchQuery]); + }, [task.id, task.title, task.filePath, plugin.settings.data.searchQuery]); // useEffect(() => { // const allSubTasks = task.body.filter(line => isTaskLine(line.trim())); @@ -195,8 +184,8 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin // // console.log("renderSubTasks : This useEffect should only run when subTask updates | Calling rendered with:\n", subtaskText); // element.empty(); // Clear previous content - // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContent.tsx - // // const searchQuery = plugin.settings.data.globalSettings.searchQuery || ''; + // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContainer.tsx + // // const searchQuery = plugin.settings.data.searchQuery || ''; // // if (searchQuery) { // // const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // // const regex = new RegExp(`(${escapedQuery})`, "gi"); @@ -253,7 +242,11 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin break; } } catch (err) { - console.error('Error rendering subtask:', err); + bugReporterManagerInsatance.addToLogs( + 123, + String(err), + "TaskItem.tsx/Sub-tasks rendering useEffect", + ); } } })(); @@ -376,97 +369,129 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin // ======================================== const getColorIndicator = useCallback(() => { - const today = new Date(); - const taskUniversalDate = parseUniversalDate(universalDate) || new Date(universalDate); - - if (taskUniversalDate.toDateString() === today.toDateString()) { - if (task.time) { - const [startStr, endStr] = task.time.contains('-') ? task.time.split('-') : [task.time, task.time]; - const [startHours, startMinutes] = startStr.contains(':') ? startStr.trim().split(':').map(Number) : [startStr, 0].map(Number); - const [endHours, endMinutes] = endStr.contains(':') ? endStr.trim().split(':').map(Number) : [endStr, 0].map(Number); - - const startTime = new Date(today); - startTime.setHours(startHours, startMinutes, 0, 0); - - const endTime = new Date(today); - endTime.setHours(endHours, endMinutes, 0, 0); - - const now = new Date(); - - if (now < startTime) { - // return 'var(--color-yellow)'; // Not started yet - return '#e8ce4aa8'; // Not started yet - } else if (now >= startTime && now <= endTime) { - return 'var(--color-blue)'; // In progress - } else if (now > endTime) { - return '#f23a3ab8'; // Past due + // Return grey if there's no universal date + if (!universalDate) { + return 'grey'; + } + + // Use robust date parser for reliable parsing + const dateFormatToUse = plugin.settings.data.dateFormat || DEFAULT_DATE_FORMAT; + const parsedTaskDate = robustDateParser(universalDate, dateFormatToUse); + + if (!parsedTaskDate) { + return 'grey'; // Invalid date, return grey + } + + // Set to start of day for accurate date comparison + const taskDateAtStartOfDay = startOfDay(parsedTaskDate); + const today = startOfDay(new Date()); + + // Check if the task date is today + if (isToday(taskDateAtStartOfDay)) { + // If there's a time component, use it for more detailed color indication + if (task.time && task.time.trim() !== "") { + try { + const [startStr, endStr] = task.time.includes('-') ? task.time.split('-') : [task.time, task.time]; + const [startHours, startMinutes] = startStr.includes(':') ? + startStr.trim().split(':').map(Number) : + [parseInt(startStr.trim()), 0]; + const [endHours, endMinutes] = endStr.includes(':') ? + endStr.trim().split(':').map(Number) : + [parseInt(endStr.trim()), 0]; + + const startTime = new Date(today); + startTime.setHours(startHours, startMinutes, 0, 0); + + const endTime = new Date(today); + endTime.setHours(endHours, endMinutes, 0, 0); + + const now = new Date(); + + if (compareAsc(now, startTime) < 0) { + // Not started yet + return 'var(--color-yellow)'; + } else if (compareAsc(now, startTime) >= 0 && compareAsc(now, endTime) <= 0) { + // In progress + return 'var(--color-blue)'; + } else if (compareAsc(now, endTime) > 0) { + // Past due + return '#f23a3ab8'; + } + } catch (error) { + // If time parsing fails, return yellow for "due today" + return 'var(--color-yellow)'; } } else { - return 'var(--color-yellow)'; // Due today but no time info + // Due today but no time info + return 'var(--color-yellow)'; } - } else if (taskUniversalDate > today) { - return 'green'; // Due in future - } else if (taskUniversalDate < today) { - // return 'var(--color-red)'; // Past due - return '#f23a3ab8'; // Past due + } else if (isAfter(taskDateAtStartOfDay, today)) { + // Due in future + return 'green'; + } else if (isBefore(taskDateAtStartOfDay, today)) { + // Past due + return '#f23a3ab8'; } else { - return 'grey'; // No due date + return 'grey'; // No due date or comparison failed } - }, [universalDate, task.time]); + }, [universalDate, task.time, plugin.settings.data.dateFormat]); // Function to get the card background color based on tags - function getCardBgBasedOnTag(tags: string[]): string | undefined { - if (plugin.settings.data.globalSettings.tagColorsType === "text") { - return undefined; - } + function getCardBgBasedOnTag(): string | undefined { + const allTags = getAllTaskTags(task); - const tagColors = plugin.settings.data.globalSettings.tagColors; + if (globalSettings.tagColorsType === TagColorType.CardBg) { - if (!Array.isArray(tagColors) || tagColors.length === 0) { - return undefined; - } + const tagColors = plugin.settings.data.tagColors; - // Prepare a map for faster lookup - const tagColorMap = new Map(tagColors.map((t) => [t.name, t])); + if (!Array.isArray(tagColors) || tagColors.length === 0) { + return undefined; + } - let highestPriorityTag: { name: string; color: string; priority: number } | undefined = undefined; + // Prepare a map for faster lookup + const tagColorMap = new Map(tagColors.map((t) => [t.name, t])); - for (const rawTag of tags) { - const tagName = rawTag.replace('#', ''); - let tagData = tagColorMap.get(tagName); + let highestPriorityTag: { name: string; color: string; priority: number } | undefined = undefined; - if (!tagData) { - tagColorMap.forEach((tagColor, tagNameKey, mapValue) => { - const result = matchTagsWithWildcards(tagNameKey, tagName || ''); - // Return the first match found - if (result) tagData = tagColor; - }); - } + for (const rawTag of allTags) { + const tagName = rawTag.replace('#', ''); + let tagData = tagColorMap.get(tagName); - if (tagData) { - if ( - !highestPriorityTag || - (tagData.priority) < (highestPriorityTag.priority) - ) { - highestPriorityTag = tagData; + if (!tagData) { + tagColorMap.forEach((tagColor, tagNameKey, mapValue) => { + const result = matchTagsWithWildcards(tagNameKey, tagName || ''); + // Return the first match found + if (result) tagData = tagColor; + }); } - } - } - const getOpacityValue = (color: string): number => { - const rgbaMatch = color.match(/rgba?\((\d+), (\d+), (\d+)(, (\d+(\.\d+)?))?\)/); - if (rgbaMatch) { - const opacity = rgbaMatch[5] ? parseFloat(rgbaMatch[5]) : 1; - return opacity; + if (tagData) { + if ( + !highestPriorityTag || + (tagData.priority) < (highestPriorityTag.priority) + ) { + highestPriorityTag = tagData; + } + } } - return 1; - }; - if (highestPriorityTag && getOpacityValue(highestPriorityTag.color) > 0.2) { - return updateRGBAOpacity(plugin, highestPriorityTag.color, 0.2); + // const getOpacityValue = (color: string): number => { + // const rgbaMatch = color.match(/rgba?\((\d+), (\d+), (\d+)(, (\d+(\.\d+)?))?\)/); + // if (rgbaMatch) { + // const opacity = rgbaMatch[5] ? parseFloat(rgbaMatch[5]) : 1; + // return opacity; + // } + // return 1; + // }; + + // if (highestPriorityTag && getOpacityValue(highestPriorityTag.color) > 0.2) { + // return updateRGBAOpacity(highestPriorityTag.color, 0.2); + // } + + return highestPriorityTag?.color; } - return highestPriorityTag?.color; + return undefined; } // ======================================== @@ -517,7 +542,11 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin handleCheckboxChange(plugin, task); } } catch (error) { - console.error("Error updating task:", error); + bugReporterManagerInsatance.addToLogs( + 124, + String(error), + "TaskItem.tsx/handleMainCheckBoxClick", + ); } // The component might be unmounted by the time this runs, but this is a safeguard. @@ -554,7 +583,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin // Toggle the checkbox status only for the specific line const symbol = extractCheckboxSymbol(line); - const nextStatus = checkboxStateSwitcher(plugin, symbol); + const nextStatus = checkboxStateSwitcher(globalSettings.customStatuses, symbol); return line.replace(`[${symbol}]`, `[${nextStatus.newSymbol}]`); } @@ -590,7 +619,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin }; const onEditButtonClicked = (event: React.MouseEvent) => { - const settingOption = plugin.settings.data.globalSettings.editButtonAction; + const settingOption = plugin.settings.data.editButtonAction; if (settingOption !== EditButtonMode.NoteInHover) { handleEditTask(plugin, task, settingOption); } else { @@ -601,7 +630,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin } const handleDoubleClickOnCard = (event: React.MouseEvent) => { - const settingOption = plugin.settings.data.globalSettings.doubleClickCardToEdit; + const settingOption = plugin.settings.data.doubleClickCardToEdit; if (settingOption === EditButtonMode.None) return; if (settingOption !== EditButtonMode.NoteInHover) { @@ -622,7 +651,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin return; } - const settingOption = plugin.settings.data.globalSettings.editButtonAction; + const settingOption = plugin.settings.data.editButtonAction; if (settingOption !== EditButtonMode.NoteInHover) { handleEditTask(plugin, childTask, settingOption); } else { @@ -631,13 +660,13 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin event.ctrlKey = false; } } catch (error) { - console.error("Error opening child task modal:", error); bugReporterManagerInsatance.showNotice(6, "Error opening child task modal", String(error), "TaskItem.tsx/handleOpenChildTaskModal"); } } const handleMenuButtonClicked = (event: React.MouseEvent) => { event.stopPropagation(); + const taskItemMenu = new Menu(); taskItemMenu.addItem((item) => { @@ -645,113 +674,182 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin item.setIsLabel(true); }); taskItemMenu.addItem((item) => { - item.setTitle(t("status")); item.setIcon("info"); + item.setTitle(t("status")); const statusMenu = item.setSubmenu() - const customStatues = getCustomStatusOptionsForDropdown(plugin.settings.data.globalSettings.customStatuses); - customStatues.forEach((status) => { + const statusOptions = getCustomStatusOptionsForDropdown( + plugin.settings.data.customStatuses, + { mode: 'flat' } // Context menus don't support optgroups, use flat + ); + + // Handle both output types for future-proofing + const options = statusOptions.type === 'flat' + ? statusOptions.options + : statusOptions.groups.flatMap(group => group.options); + + options.forEach((status) => { statusMenu.addItem((item) => { - item.setTitle(status.text); - // item.setIcon("eye-off"); // TODO : In future map lucude-icons with the ITS theme emoji icons for custom statuses. + // Render status with markdown formatting + MarkdownUIRenderer.renderSubtaskText( + plugin.app, + `- [${status.value}] ${status.label}`, + item.titleEl, + '', + null + ); + item.onClick(() => { - updateTaskItemStatus(plugin, task, status.value); - }) + updateTaskItemStatus(plugin, task, task, status.value); + }); + }); + }); + + // Optional: Add visual separator groups if using grouped mode + if (statusOptions.type === 'grouped') { + // Rebuild menu with group separators (requires clearing and re-adding) + // Note: Obsidian Menu API doesn't natively support optgroups, + // so this is a visual workaround using disabled items + } + }); + + // Priority submenu + taskItemMenu.addItem((item) => { + item.setIcon("flag"); + item.setTitle(t("priority")); + const priMenu = item.setSubmenu(); + const priorityOptions = getPriorityOptionsForDropdown(); + priorityOptions.forEach((p) => { + priMenu.addItem((it) => { + it.setTitle(p.text); + it.onClick(() => updateTaskItemPriority(plugin, task, task, p.value)); }); - }) + }); + }); + + // Tags editor modal - TODO : It doesnt make sense to build another modal specifically changing the tags, when the AddOrEditTaskModal can itself do this. + // taskItemMenu.addItem((item) => { + // item.setTitle(t("tags")); + // item.setIcon("tag"); + // item.onClick(() => { + // const modal = new EditTagsModal(plugin, task.tags || [], (newTags: string[]) => { + // updateTaskItemTags(plugin, task, task, newTags.map((tg) => (tg.startsWith('#') ? tg : `#${tg}`))); + // }); + // modal.open(); + // }); + // }); + + // Dates submenu + + taskItemMenu.addItem((it) => { + it.setIcon("calendar-plus") + it.setTitle(t("start-date")); + it.onClick(async () => { + openDateInputModal(plugin, t("start"), (newDate: string) => { + updateTaskItemDate(plugin, task, task, UniversalDateOptions.startDate, newDate); + }, task.startDate) + }); + }); + taskItemMenu.addItem((it) => { + it.setIcon("calendar-clock") + it.setTitle(t("scheduled-date")); + it.onClick(async () => { + openDateInputModal(plugin, t("scheduled"), (newDate: string) => { + updateTaskItemDate(plugin, task, task, UniversalDateOptions.scheduledDate, newDate); + }, task.scheduledDate) + }); + }); + taskItemMenu.addItem((it) => { + it.setIcon("calendar") + it.setTitle(t("due-date")); + it.onClick(async () => { + openDateInputModal(plugin, t("due"), (newDate: string) => { + updateTaskItemDate(plugin, task, task, UniversalDateOptions.dueDate, newDate); + }, task.due) + }); + }); + + // Reminder item - open prompt for date/time + taskItemMenu.addItem((item) => { + item.setIcon("clock"); + item.setTitle(t("reminder")); + item.onClick(async () => { + const modal = new DateTimePickerModal(plugin, t("reminder"), task.reminder); + modal.onDateTimeSelected = (dateTime) => { // e.g., "2024-01-15T14:30" or "14:30" + updateTaskItemReminder(plugin, task, task, dateTime); + }; + modal.open(); + }); }); taskItemMenu.addSeparator(); taskItemMenu.addItem((item) => { - item.setTitle(t("quick-actions")); + item.setTitle(t("task-actions")); item.setIsLabel(true); }); taskItemMenu.addItem((item) => { + item.setIcon("copy"); item.setTitle(t("copy-task-title")); - item.setIcon("eye-off"); item.onClick(async () => { + try { + await navigator.clipboard.writeText(cleanTaskTitleLegacy(task)); + new Notice(t("copy-task-title-successful")); + } catch (error) { + new Notice(t("copy-task-title-unsuccessful")); + } }); - - // Priority submenu - taskItemMenu.addItem((item) => { - item.setTitle(t("priority")); - item.setIcon("flag"); - const priMenu = item.setSubmenu(); - const priorities = [ - { label: t("none"), value: 0 }, - { label: t("low"), value: 1 }, - { label: t("medium"), value: 2 }, - { label: t("high"), value: 3 }, - ]; - priorities.forEach((p) => { - priMenu.addItem((it) => { - it.setTitle(p.label); - it.onClick(() => updateTaskItemPriority(plugin, task, p.value)); - }); - }); + }); + taskItemMenu.addItem((item) => { + item.setIcon("square-pen"); + item.setTitle(t("open-task-editor")); + item.onClick(async () => { + handleEditTask(plugin, task, EditButtonMode.Modal); }); - - // Dates submenu - taskItemMenu.addItem((item) => { - item.setTitle(t("dates")); - item.setIcon("calendar"); - const dateMenu = item.setSubmenu(); - dateMenu.addItem((it) => { - it.setTitle(t("start-date")); - it.onClick(async () => { - const newDate = await showTextInputModal(plugin.app, { title: t("set-start-date"), placeholder: 'YYYY-MM-DD', initialValue: task.startDate || '' }); - if (newDate) updateTaskItemDate(plugin, task, 'startDate', newDate); - }); - }); - dateMenu.addItem((it) => { - it.setTitle(t("scheduled-date")); - it.onClick(async () => { - const newDate = await showTextInputModal(plugin.app, { title: t("set-scheduled-date"), placeholder: 'YYYY-MM-DD', initialValue: task.scheduledDate || '' }); - if (newDate) updateTaskItemDate(plugin, task, 'scheduledDate', newDate); - }); - }); - dateMenu.addItem((it) => { - it.setTitle(t("due-date")); - it.onClick(async () => { - const newDate = await showTextInputModal(plugin.app, { title: t("set-due-date"), placeholder: 'YYYY-MM-DD', initialValue: task.due || '' }); - if (newDate) updateTaskItemDate(plugin, task, 'due', newDate); - }); - }); + }); + taskItemMenu.addItem((item) => { + item.setIcon("square-pen"); + item.setTitle(t("open-task-editor-in")); + const taskEditorMenu = item.setSubmenu(); + taskEditorMenu.addItem((subItem) => { + subItem.setIcon("columns-2"); + subItem.setTitle(t("right-split")); + subItem.onClick(() => handleEditTask(plugin, task, EditButtonMode.ViewInSplitTab)); }); - // Reminder item - open prompt for date/time - taskItemMenu.addItem((item) => { - item.setTitle(t("reminder")); - item.setIcon("clock"); - item.onClick(async () => { - const newReminder = await showTextInputModal(plugin.app, { title: t("set-reminder"), placeholder: 'YYYY-MM-DD HH:mm', initialValue: task.reminder || '' }); - if (newReminder) updateTaskItemReminder(plugin, task, newReminder); - }); + taskEditorMenu.addItem((subItem) => { + subItem.setIcon("picture-in-picture-2"); + subItem.setTitle(t("new-window")); + subItem.onClick(() => handleEditTask(plugin, task, EditButtonMode.ViewInWindow)); }); + }); - // Tags editor modal - taskItemMenu.addItem((item) => { - item.setTitle(t("tags")); - item.setIcon("tag"); - item.onClick(() => { - const modal = new EditTagsModal(plugin, task.tags || [], (newTags: string[]) => { - updateTaskItemTags(plugin, task, task, newTags.map((tg) => (tg.startsWith('#') ? tg : `#${tg}`))); - }); - modal.open(); - }); - }); + taskItemMenu.addSeparator(); + + taskItemMenu.addItem((item) => { + item.setTitle(t("note-actions")); + item.setIsLabel(true); }); + taskItemMenu.addItem((item) => { + item.setIcon("file-input"); item.setTitle(t("open-note")); - item.setIcon("eye-off"); item.onClick(async () => { + handleEditTask(plugin, task, EditButtonMode.NoteInTab) }); }); + taskItemMenu.addItem((item) => { + item.setIcon("columns-2"); + item.setTitle(t("open-note-to-right")); + item.onClick(async () => { + handleEditTask(plugin, task, EditButtonMode.NoteInSplit) + }); + }); + // Note actions submenu taskItemMenu.addItem((item) => { - item.setTitle(t("contextMenus.task.noteActions")); item.setIcon("file-text"); + item.setTitle(t("more-note-actions")); const submenu = (item as any).setSubmenu(); @@ -767,16 +865,16 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin } // Add common file actions (these will either supplement or replace the native menu) - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.rename")); + submenu.addItem((subItem: MenuItem) => { subItem.setIcon("pencil"); + subItem.setTitle(t("rename-note")); subItem.onClick(async () => { try { // Modal-based rename const currentName = file.basename; const newName = await showTextInputModal(plugin.app, { - title: t("contextMenus.task.renameTitle"), - placeholder: t("contextMenus.task.renamePlaceholder"), + title: t("rename-note"), + placeholder: t("rename-note-placeholder"), initialValue: currentName, }); @@ -794,115 +892,32 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin // Rename the file await plugin.app.vault.rename(file, newPath); - new Notice( - t("contextMenus.task.notices.renameSuccess") + finalName, - - ); - - // // Trigger update callback - // if (options.onUpdate) { - // options.onUpdate(); - // } + new Notice("File renamed successfully."); } } catch (error) { - console.error("Error renaming file:", error); - new Notice(t("contextMenus.task.notices.renameFailure")); + new Notice("There was an error while renaming the file."); + bugReporterManagerInsatance.addToLogs( + 125, + String(error), + "TaskItem.tsx/handleMenuButtonClicked/renaming", + ); } }); }); - submenu.addItem((subItem: any) => { - subItem.setTitle(t("delete-note")); + submenu.addItem((subItem: MenuItem) => { subItem.setIcon("trash"); + subItem.setTitle(t("delete-note")); subItem.onClick(async () => { - // Show confirmation and delete - const confirmed = await showConfirmationModal(plugin.app, { - title: t("contextMenus.task.deleteTitle"), - message: t("contextMenus.task.deleteMessage") + file.name, - confirmText: t("contextMenus.task.deleteConfirm"), - cancelText: t("common.cancel"), - isDestructive: true, - }); - if (confirmed) { - plugin.app.vault.trash(file, true); - } - }); - }); - - submenu.addSeparator(); - - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.copyPath")); - subItem.setIcon("copy"); - subItem.onClick(async () => { - try { - await navigator.clipboard.writeText(file.path); - new Notice(t("contextMenus.task.notices.copyPathSuccess")); - } catch (error) { - new Notice(t("contextMenus.task.notices.copyFailure")); - } - }); - }); - - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.copyUrl")); - subItem.setIcon("link"); - subItem.onClick(async () => { - try { - const url = `obsidian://open?vault=${encodeURIComponent(plugin.app.vault.getName())}&file=${encodeURIComponent(file.path)}`; - await navigator.clipboard.writeText(url); - new Notice(t("contextMenus.task.notices.copyUrlSuccess")); - } catch (error) { - new Notice(t("contextMenus.task.notices.copyFailure")); - } - }); - }); - - submenu.addSeparator(); - - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.showInExplorer")); - subItem.setIcon("folder-open"); - subItem.onClick(() => { - // Reveal file in file explorer - plugin.app.workspace - .getLeaf() - .setViewState({ - type: "file-explorer", - state: {}, - }) - .then(() => { - // Focus the file in the explorer - const fileExplorer = - plugin.app.workspace.getLeavesOfType("file-explorer")[0]; - if (fileExplorer?.view && "revealInFolder" in fileExplorer.view) { - (fileExplorer.view as any).revealInFolder(file); - } - }); + plugin.app.vault.trash(file, true).then(() => { + new Notice("File deleted successfully. Moved to system trash."); + }) + // handleDeleteTask(plugin, task, true); }); }); } }); - // // Show minimize or maximize option based on current state - // if (columnData.minimized) { - // taskItemMenu.addItem((item) => { - // item.setTitle(t("maximize-column")); - // item.setIcon("panel-left-open"); - // item.onClick(async () => { - // await handleMinimizeColumn(); - // }); - // }); - // } else { - // taskItemMenu.addItem((item) => { - // item.setTitle(t("minimize-column")); - // item.setIcon("panel-left-close"); - // item.onClick(async () => { - // await handleMinimizeColumn(); - // }); - // }); - // } - // Use native event if available (React event has nativeEvent property) taskItemMenu.showAtMouseEvent( (event instanceof MouseEvent ? event : event.nativeEvent) @@ -911,58 +926,41 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin // Handlers for drag and drop const handleDragStart = useCallback((e: React.DragEvent) => { - console.log("TaskItem : handleDragStart..."); + // prevent column drag from also starting + e.stopPropagation(); + if (!columnData) { e.preventDefault(); - console.warn('handleDragStart: columnData is undefined'); + bugReporterManagerInsatance.addToLogs(91, `Column data : undefined`, "TaskItem.tsx/handleDragStart"); return; } - - setIsDragging(true); - // Delegate to manager for standardized behavior (sets current payload and dims element) try { const el = taskItemRef.current as HTMLDivElement; - const payload: currentDragDataPayload = { task, taskIndex: String(dataAttributeIndex), sourceColumnData: columnData, currentBoardIndex: activeBoardSettings.index, swimlaneData: swimlaneData }; - dragDropTasksManagerInsatance.handleDragStartEvent(e.nativeEvent as DragEvent, el, payload, 0); - - // Add dragging class after a small delay to not affect the drag image - const clone = el.cloneNode(true) as HTMLDivElement; - e.dataTransfer?.setDragImage(el, 0, 0); - requestAnimationFrame(() => { - clone.classList.add("task-item-dragging"); - console.log("TaskItem : handleDragStart... done : ", el); - }); - - // Also set a drag image from the whole task element so the preview is the full card - // TODO : The drag image is taking too much width and also its still in its default state, like very dimmed opacity. Improve it to get a nice border and increase the opacity so it looks more real. - // if (taskItemRef.current && e.dataTransfer) { - // console.log("TaskItemRef.current", taskItemRef.current); - // const clone = taskItemRef.current.cloneNode(true) as HTMLElement; - // // clone.style.boxShadow = '0 8px 16px rgba(0,0,0,0.12)'; - // clone.style.opacity = '0.5'; - // clone.style.position = 'absolute'; - // // clone.style.top = '-9999px'; - // // document.body.appendChild(clone); - // const rect = taskItemRef.current.getBoundingClientRect(); - // e.dataTransfer.setDragImage(clone, rect.width, rect.height); - // setTimeout(() => { - // try { document.body.removeChild(clone); } catch { } - // }, 0); - // } + const payload: currentDragDataPayload = { + task, + taskIndex: String(dataAttributeIndex), + sourceColumnData: columnData, + currentViewIndex: activeViewIndex, + currentBoardID: activeBoardID, + swimlaneData: swimlaneData + }; + // Delegate to manager for standardized behavior (sets current payload and dims element) + dragDropTasksManagerInsatance.handleDragStartEvent(e.nativeEvent as DragEvent, el, payload); } catch (err) { // fallback minimal behavior // try { // e.dataTransfer.setData('application/json', JSON.stringify({ task, sourceColumnData: columnData })); // e.dataTransfer.effectAllowed = 'move'; // } catch (ex) {/* ignore */ } - - console.error(err); + bugReporterManagerInsatance.addToLogs( + 126, + String(err), + "TaskItem.tsx/handleDragStart", + ); } }, [task, columnData]); const handleDragEnd = useCallback(() => { - console.log("TaskItem : handleDragEnd..."); - setIsDragging(false); // Remove dim effect from this dragged task and clear manager state if (taskItemRef.current) { @@ -982,80 +980,94 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin try { return (
-
- {/* Render priority */} - {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Priority) && task.priority > 0 && ( -
{priorityEmojis[task.priority as number]}
- )} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.FilePathInHeader) && task.filePath && ( +
+
{task.filePath.split('/').pop()}
+
+ )} - {/* Render tags individually */} - {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Tags) && task.tags.length > 0 && ( -
- {/* Render line tags (editable) */} - {task.tags.map((tag: string) => { - const tagName = tag.replace('#', ''); - const customTag = plugin.settings.data.globalSettings.tagColorsType === "text" ? plugin.settings.data.globalSettings.tagColors.find(t => t.name === tagName) : undefined; - const tagColor = customTag?.color || `var(--tag-color)`; - const backgroundColor = customTag ? updateRGBAOpacity(plugin, customTag.color, 0.1) : `var(--tag-background)`; // 10% opacity background - const borderColor = customTag ? updateRGBAOpacity(plugin, customTag.color, 0.5) : `var(--tag-color-hover)`; - - // If columnIndex is defined, proceed to get the column - if ( - (!activeBoardSettings?.showColumnTags) && - columnData && - columnData?.colType === colTypeNames.namedTag && - tagName.replace('#', '') === columnData?.coltag?.replace('#', '') - ) { - return null; - } - - const tagKey = `${task.id}-${tag}`; - // Render the remaining tags - return ( -
- {tag} -
- ); - })} - - {/* Render frontmatter tags (read-only) */} - {task.frontmatterTags && task.frontmatterTags.map((tag: string) => { - const tagKey = `${task.id}-fm-${tag}`; - // Render frontmatter tags with different styling - return ( -
- {tag} -
- ); - })} -
- )} -
+
+
+ {/* Render priority */} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Priority) && task.priority > 0 && ( +
{priorityEmojis[task.priority as number]}
+ )} -
- {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ID) && task.legacyId && ( -
-
ID
{task.legacyId}
-
- )} + {/* Render tags individually */} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Tags) && (task.tags.length > 0 || task.frontmatterTags.length > 0) && ( +
+ {/* Render line tags (editable) */} + {task.tags.map((tag: string) => { + const isTagBg = globalSettings.tagColorsType === TagColorType.TagBg; + const isCardBg = globalSettings.tagColorsType === TagColorType.CardBg; + const taskTag = tag.replace('#', '').toLowerCase(); + const columnTag = columnData?.coltag?.replace('#', '').toLowerCase(); + + const customTag = isCardBg ? undefined : plugin.settings.data.tagColors.find(t => t.name.replace('#', '').toLowerCase() === taskTag); + + const tagColor = customTag?.color; + const dimmedTagColor = customTag ? updateRGBAOpacity(customTag.color, 0.1) : undefined; // 10% opacity background + // const borderColor = customTag ? updateRGBAOpacity(customTag.color, 0.5) : `var(--tag-color-hover)`; + + // If columnIndex is defined, proceed to get the column + if ( + activeViewType === viewTypeNames.kanban && + kanbanViewData && + kanbanViewData.showColumnTags && + columnData && + columnData?.colType === colTypeNames.namedTag && + taskTag === columnTag + ) { + return null; + } + + const tagKey = `${task.id}-${tag}`; + // Render the remaining tags + return ( +
+ {tag} +
+ ); + })} + + {/* Render frontmatter tags (read-only) */} + {task.frontmatterTags.map((tag: string) => { + const tagKey = `${task.id}-fm-${tag}`; + // Render frontmatter tags with different styling + return ( +
+ {tag} +
+ ); + })} +
+ )} +
+
+ {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ID) && task.legacyId && ( +
+
ID
{task.legacyId}
+
+ )} +
+
); } catch (error) { - // bugReporterManagerInsatance.showNotice(7, "Error while rendering task header", error as string, "TaskItem.tsx/renderHeader"); - console.warn("TaskItem.tsx/renderHeader : Error while rendering task header", error); + bugReporterManagerInsatance.addToLogs(7, error as string, "TaskItem.tsx/renderHeader"); return null; } }; @@ -1114,7 +1126,8 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin const tabMatchInTitle = task.title.match(new RegExp(`^(${tabString})+`)); const titleTabs = tabMatchInTitle && tabMatchInTitle[0] ? tabMatchInTitle[0].length / tabString.length : 0; const numTabs = tabMatch && tabMatch[0] ? tabMatch[0].length / tabString.length : 0; - const paddingLeft = numTabs > 1 ? `${(numTabs - titleTabs - 1) * 15}px` : '0px'; + const numOfTabs = isTaskNote ? numTabs + 1 : numTabs; + const paddingLeft = numOfTabs > 1 ? `${(numOfTabs - titleTabs - 1) * 15}px` : '0px'; // Create a unique key for this subtask based on task.id and index const uniqueKey = `${task.id}-${index}`; @@ -1123,7 +1136,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin
= ({ dataAttributeIndex, plugin ); } catch (error) { - // bugReporterManagerInsatance.showNotice(8, "Error while rendering sub-tasks", error as string, "TaskItem.tsx/renderSubTasks"); - console.warn("TaskItem.tsx/renderSubTasks : Error while rendering sub-tasks", error); + bugReporterManagerInsatance.addToLogs(8, error as string, "TaskItem.tsx/renderSubTasks"); return null; } }; @@ -1174,7 +1186,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Status) && task?.status && (
{/*
{t("status")}
*/} -
{getStatusNameFromStatusSymbol(task.status, globalSettings)}
+
{getStatusNameFromStatusSymbol(task.status, globalSettings.customStatuses ?? [])}
)} {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Reminder) && task?.reminder && ( @@ -1226,14 +1238,23 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin 📄
{task.filePath.split('/').pop()}
)} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ParentFolder) && task.filePath && ( +
+ 📁
{task.filePath.split('/')[task.filePath.split('/').length - 2] ? task.filePath.split('/')[task.filePath.split('/').length - 2] : "Vault root"}
+
+ )} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.FullPath) && task.filePath && ( +
+ 📁
{task.filePath.split('/').slice(0, -1).join("/") ? task.filePath.split('/').slice(0, -1).join("/") : "Vault root"}
+
+ )}
)} ); } catch (error) { - // bugReporterManagerInsatance.showNotice(9, "Error while rendering task footer", error as string, "TaskItem.tsx/renderFooter"); - console.warn("TaskItem.tsx/renderFooter : Error while rendering task footer", error); + bugReporterManagerInsatance.addToLogs(9, error as string, "TaskItem.tsx/renderFooter"); return null; } }; @@ -1261,7 +1282,7 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin const renderChildTasks = () => { try { // Render only if the last viewed history is Kanban and there are child tasks - if (plugin.settings.data.globalSettings.lastViewHistory.viewedType === viewTypeNames.kanban && task?.dependsOn && task.dependsOn.length > 0) { + if (activeViewType === viewTypeNames.kanban && task?.dependsOn && task.dependsOn.length > 0) { return (
{/* Placeholder for future child tasks rendering */} @@ -1295,30 +1316,32 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin return null; } } catch (error) { - // bugReporterManagerInsatance.showNotice(10, "Error while rendering child-tasks", error as string, "TaskItem.tsx/renderChildTasks"); - console.warn("TaskItem.tsx/renderChildTasks : Error while rendering child-tasks", error); + bugReporterManagerInsatance.addToLogs(10, error as string, "TaskItem.tsx/renderChildTasks"); return null; } }; // Memoize the render functions to prevent unnecessary re-renders - const memoizedRenderHeader = useMemo(() => renderHeader(), [plugin.settings.data.globalSettings.visiblePropertiesList, task.priority, task.tags, activeBoardSettings]); - const memoizedRenderSubTasks = useMemo(() => renderSubTasks(), [plugin.settings.data.globalSettings.visiblePropertiesList, task.body, showSubtasks]); + const memoizedRenderHeader = useMemo(() => renderHeader(), [plugin.settings.data.visiblePropertiesList, task.priority, task.tags, columnData]); + const memoizedRenderSubTasks = useMemo(() => renderSubTasks(), [plugin.settings.data.visiblePropertiesList, task.body, showSubtasks]); const memoizedRenderChildTasks = useMemo(() => renderChildTasks(), [task.dependsOn, childTasksData]); - // const memoizedRenderFooter = useMemo(() => renderFooter(), [plugin.settings.data.globalSettings.showFooter, task.completion, universalDate, task.time]); + // const memoizedRenderFooter = useMemo(() => renderFooter(), [plugin.settings.data.showFooter, task.completion, universalDate, task.time]); // ======================================== - // RETURN STATEMENT (UPDATED) + // RETURN STATEMENT // ======================================== return (
@@ -1326,31 +1349,15 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin {memoizedRenderHeader} {/* Drag Handle and Task Menu button */} - {plugin.settings.data.globalSettings.experimentalFeatures && ( - <> - { - Platform.isPhone || plugin.settings.data.globalSettings.lastViewHistory.viewedType === viewTypeNames.map ? ( - <> -
- - ) : ( - <> - {/* Drag Handle */} - {columnData?.colType !== colTypeNames.allPending && ( -
- -
- )} - - ) - } - - )} + { + Platform.isDesktopApp && activeViewType !== viewTypeNames.map ? ( +
+ +
+ ) : ( +
+ ) + } {/* Task Content */}
@@ -1359,9 +1366,9 @@ const TaskItem: React.FC = ({ dataAttributeIndex, plugin { diff --git a/src/components/KanbanView/TaskItemV2.tsx b/src/components/TaskCard/TaskItemV2.tsx similarity index 63% rename from src/components/KanbanView/TaskItemV2.tsx rename to src/components/TaskCard/TaskItemV2.tsx index e0a72c1d..ecc2de2b 100644 --- a/src/components/KanbanView/TaskItemV2.tsx +++ b/src/components/TaskCard/TaskItemV2.tsx @@ -2,78 +2,77 @@ import { FaEdit, FaTrash } from 'react-icons/fa'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { checkboxStateSwitcher, extractCheckboxSymbol, getObsidianIndentationSetting, isTaskCompleted, isTaskLine } from 'src/utils/CheckBoxUtils'; -import { handleCheckboxChange, handleDeleteTask, handleSubTasksChange } from 'src/utils/taskLine/TaskItemEventHandlers'; -import { hookMarkdownLinkMouseEventHandlers, markdownButtonHoverPreviewEvent } from 'src/services/MarkdownHoverPreview'; - -import { Component, Notice, Platform, Menu, TFile } from 'obsidian'; -import { MarkdownUIRenderer } from 'src/services/MarkdownUIRenderer'; -import { cleanTaskTitleLegacy } from 'src/utils/taskLine/TaskContentFormatter'; -import { updateRGBAOpacity } from 'src/utils/UIHelpers'; -import { t } from 'src/utils/lang/helper'; -import TaskBoard from 'main'; -import { Board } from 'src/interfaces/BoardConfigs'; -import { TaskRegularExpressions, TASKS_PLUGIN_DEFAULT_SYMBOLS } from 'src/regularExpressions/TasksPluginRegularExpr'; -import { getStatusNameFromStatusSymbol, isTaskNotePresentInTags } from 'src/utils/taskNote/TaskNoteUtils'; -import { allowedFileExtensionsRegEx } from 'src/regularExpressions/MiscelleneousRegExpr'; -import { bugReporter } from 'src/services/OpenModals'; +import { Component, Notice, Platform, Menu, TFile, MenuItem } from 'obsidian'; import { ChevronDown, EllipsisVertical, Grip } from 'lucide-react'; -import { EditButtonMode, viewTypeNames, colTypeNames, taskPropertiesNames } from 'src/interfaces/Enums'; -import { getCustomStatusOptionsForDropdown, priorityEmojis } from 'src/interfaces/Mapping'; -import { taskItem, UpdateTaskEventData } from 'src/interfaces/TaskItem'; -import { matchTagsWithWildcards, verifySubtasksAndChildtasksAreComplete } from 'src/utils/algorithms/ScanningFilterer'; -import { handleTaskNoteStatusChange, handleTaskNoteBodyChange } from 'src/utils/taskNote/TaskNoteEventHandlers'; -import { eventEmitter } from 'src/services/EventEmitter'; -import { RxDragHandleDots2 } from 'react-icons/rx'; -import { getUniversalDateEmoji, getUniversalDateFromTask, parseUniversalDate } from 'src/utils/DateTimeCalculations'; -import { getTaskFromId } from 'src/utils/TaskItemUtils'; -import { handleEditTask, updateTaskItemStatus, updateTaskItemPriority, updateTaskItemDate, updateTaskItemReminder, updateTaskItemTags } from 'src/utils/UserTaskEvents'; -import EditTagsModal from 'src/modals/EditTagsModal'; - -// Helper modal functions may be provided elsewhere; declare them for TypeScript -declare function showTextInputModal(app: any, options: { title?: string; placeholder?: string; initialValue?: string }): Promise; -declare function showConfirmationModal(app: any, options: any): Promise; -import { dragDropTasksManagerInsatance, currentDragDataPayload } from 'src/managers/DragDropTasksManager'; -import { bugReporterManagerInsatance } from 'src/managers/BugReporter'; +import { isToday, isBefore, isAfter, startOfDay, compareAsc } from 'date-fns'; +import { t } from 'i18next'; +import TaskBoard from '../../../main.js'; +import { KanbanView } from '../../interfaces/BoardConfigs.js'; +import { DEFAULT_DATE_FORMAT } from '../../interfaces/Constants.js'; +import { taskPropertiesNames, TagColorType, EditButtonMode, UniversalDateOptions, viewTypeNames, colTypeNames } from '../../interfaces/Enums.js'; +import { getCustomStatusOptionsForDropdown, getPriorityOptionsForDropdown } from '../../interfaces/Mapping.js'; +import { taskItem, UpdateTaskEventData } from '../../interfaces/TaskItem.js'; +import { bugReporterManagerInsatance } from '../../managers/BugReporter.js'; +import { currentDragDataPayload, dragDropTasksManagerInsatance } from '../../managers/DragDropTasksManager.js'; +import { DateTimePickerModal } from '../../modals/date_time_picker/DateTimePickerModal.js'; +import { showTextInputModal } from '../../modals/TextInputModal.js'; +import { TaskRegularExpressions, TASKS_PLUGIN_DEFAULT_SYMBOLS } from '../../regularExpressions/TasksPluginRegularExpr.js'; +import { eventEmitter } from '../../services/EventEmitter.js'; +import { hookMarkdownLinkMouseEventHandlers, markdownButtonHoverPreviewEvent } from '../../services/MarkdownHoverPreview.js'; +import { MarkdownUIRenderer } from '../../services/MarkdownUIRenderer.js'; +import { openDateInputModal } from '../../services/OpenModals.js'; +import { matchTagsWithWildcards, verifySubtasksAndChildtasksAreComplete } from '../../utils/algorithms/ScanningFilterer.js'; +import { isTaskCompleted, isTaskLine, extractCheckboxSymbol, checkboxStateSwitcher, getObsidianIndentationSetting } from '../../utils/CheckBoxUtils.js'; +import { getUniversalDateFromTask, robustDateParser } from '../../utils/DateTimeCalculations.js'; +import { getAllTaskTags, getTaskFromId } from '../../utils/TaskItemUtils.js'; +import { cleanTaskTitleLegacy } from '../../utils/taskLine/TaskContentFormatter.js'; +import { handleCheckboxChange, handleDeleteTask, handleSubTasksChange } from '../../utils/taskLine/TaskItemEventHandlers.js'; +import { handleTaskNoteStatusChange, handleTaskNoteBodyChange } from '../../utils/taskNote/TaskNoteEventHandlers.js'; +import { isTaskNotePresentInTags, getStatusNameFromStatusSymbol } from '../../utils/taskNote/TaskNoteUtils.js'; +import { updateRGBAOpacity } from '../../utils/UIHelpers.js'; +import { handleEditTask, updateTaskItemStatus, updateTaskItemPriority, updateTaskItemDate, updateTaskItemReminder } from '../../utils/UserTaskEvents.js'; export interface swimlaneDataProp { property: string; value: string; } -export interface TaskProps { +export interface TaskCardProps { dataAttributeIndex: number; plugin: TaskBoard; task: taskItem; - activeBoardSettings: Board; + activeBoardID: string; + activeViewIndex: number; + activeViewType: string; + // These are optional as this TaskItem can be shown either on Kanban view or Map view. + kanbanViewData?: KanbanView; columnIndex?: number; swimlaneData?: swimlaneDataProp; } -const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, activeBoardSettings, columnIndex, swimlaneData }) => { - const globalSettings = plugin.settings.data.globalSettings; - const taskNoteIdentifierTag = plugin.settings.data.globalSettings.taskNoteIdentifierTag; +const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, activeBoardID, activeViewIndex, activeViewType, kanbanViewData, columnIndex, swimlaneData }) => { + const globalSettings = plugin.settings.data; + const taskNoteIdentifierTag = plugin.settings.data.taskNoteIdentifierTag; const isTaskNote = isTaskNotePresentInTags(taskNoteIdentifierTag, task.tags); const isThistaskCompleted = isTaskNote ? isTaskCompleted(task.status, true, plugin.settings) : isTaskCompleted(task.title, false, plugin.settings) - const columnData = columnIndex !== undefined ? activeBoardSettings?.columns[columnIndex - 1] : undefined; + const columnData = columnIndex !== undefined && kanbanViewData ? kanbanViewData.columns[columnIndex - 1] : undefined; const showDescriptionSection = globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Description) ?? true; - const [isDragging, setIsDragging] = useState(false); const [isChecked, setIsChecked] = useState(isThistaskCompleted); const [cardLoadingAnimation, setCardLoadingAnimation] = useState(false); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); - const [showSubtasks, setShowSubtasks] = useState(plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)); + const [showSubtasks, setShowSubtasks] = useState(plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)); useEffect(() => { - if (plugin.settings.data.globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)) { + if (plugin.settings.data.visiblePropertiesList?.includes(taskPropertiesNames.SubTasks)) { setShowSubtasks(true); } else { setShowSubtasks(false); } - }, [plugin.settings.data.globalSettings]); + }, [plugin.settings.data]); - const [universalDate, setUniversalDate] = useState(() => getUniversalDateFromTask(task, plugin)); + const [universalDate, setUniversalDate] = useState(() => getUniversalDateFromTask(task, globalSettings.universalDate)); useEffect(() => { - setUniversalDate(getUniversalDateFromTask(task, plugin)); + setUniversalDate(getUniversalDateFromTask(task, globalSettings.universalDate)); }, [task.due, task.startDate, task.scheduledDate]); @@ -115,8 +114,8 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act // if (titleElement && task.title !== "") { // let cleanedTitle = cleanTaskTitleLegacy(task); - // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContent.tsx - // // const searchQuery = plugin.settings.data.globalSettings.searchQuery || ''; + // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContainer.tsx + // // const searchQuery = plugin.settings.data.searchQuery || ''; // // if (searchQuery) { // // const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // // const regex = new RegExp(`(${escapedQuery})`, "gi"); @@ -156,7 +155,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act el.innerHTML = ''; if (task.title === "") return; - const cleanedTitle = cleanTaskTitleLegacy(task); + const cleanedTitle = isTaskNote ? task.title : cleanTaskTitleLegacy(task); await MarkdownUIRenderer.renderTaskDisc( plugin.app, @@ -173,14 +172,18 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act hookMarkdownLinkMouseEventHandlers(plugin.app, plugin, el, task.filePath, task.filePath); } catch (err) { - console.error('Error rendering task title:', err); + bugReporterManagerInsatance.addToLogs( + 122, + String(err), + "TaskItemV2.tsx/Main title rendering useEffect", + ); } })(); // return () => { // cancelled = true; // }; - }, [task.id, task.title, task.filePath, plugin.settings.data.globalSettings.searchQuery]); + }, [task.id, task.title, task.filePath, plugin.settings.data.searchQuery]); // useEffect(() => { // const allSubTasks = task.body.filter(line => isTaskLine(line.trim())); @@ -195,8 +198,8 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act // // console.log("renderSubTasks : This useEffect should only run when subTask updates | Calling rendered with:\n", subtaskText); // element.empty(); // Clear previous content - // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContent.tsx - // // const searchQuery = plugin.settings.data.globalSettings.searchQuery || ''; + // // NOTE : This search method is not working smoothly, hence using the first approach in file TaskBoardViewContainer.tsx + // // const searchQuery = plugin.settings.data.searchQuery || ''; // // if (searchQuery) { // // const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // // const regex = new RegExp(`(${escapedQuery})`, "gi"); @@ -253,7 +256,11 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act break; } } catch (err) { - console.error('Error rendering subtask:', err); + bugReporterManagerInsatance.addToLogs( + 123, + String(err), + "TaskItemV2.tsx/Sub-tasks rendering useEffect", + ); } } })(); @@ -376,97 +383,129 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act // ======================================== const getColorIndicator = useCallback(() => { - const today = new Date(); - const taskUniversalDate = parseUniversalDate(universalDate) || new Date(universalDate); - - if (taskUniversalDate.toDateString() === today.toDateString()) { - if (task.time) { - const [startStr, endStr] = task.time.contains('-') ? task.time.split('-') : [task.time, task.time]; - const [startHours, startMinutes] = startStr.contains(':') ? startStr.trim().split(':').map(Number) : [startStr, 0].map(Number); - const [endHours, endMinutes] = endStr.contains(':') ? endStr.trim().split(':').map(Number) : [endStr, 0].map(Number); - - const startTime = new Date(today); - startTime.setHours(startHours, startMinutes, 0, 0); - - const endTime = new Date(today); - endTime.setHours(endHours, endMinutes, 0, 0); - - const now = new Date(); - - if (now < startTime) { - // return 'var(--color-yellow)'; // Not started yet - return '#e8ce4aa8'; // Not started yet - } else if (now >= startTime && now <= endTime) { - return 'var(--color-blue)'; // In progress - } else if (now > endTime) { - return '#f23a3ab8'; // Past due + // Return grey if there's no universal date + if (!universalDate) { + return 'grey'; + } + + // Use robust date parser for reliable parsing + const dateFormatToUse = plugin.settings.data.dateFormat || DEFAULT_DATE_FORMAT; + const parsedTaskDate = robustDateParser(universalDate, dateFormatToUse); + + if (!parsedTaskDate) { + return 'grey'; // Invalid date, return grey + } + + // Set to start of day for accurate date comparison + const taskDateAtStartOfDay = startOfDay(parsedTaskDate); + const today = startOfDay(new Date()); + + // Check if the task date is today + if (isToday(taskDateAtStartOfDay)) { + // If there's a time component, use it for more detailed color indication + if (task.time && task.time.trim() !== "") { + try { + const [startStr, endStr] = task.time.includes('-') ? task.time.split('-') : [task.time, task.time]; + const [startHours, startMinutes] = startStr.includes(':') ? + startStr.trim().split(':').map(Number) : + [parseInt(startStr.trim()), 0]; + const [endHours, endMinutes] = endStr.includes(':') ? + endStr.trim().split(':').map(Number) : + [parseInt(endStr.trim()), 0]; + + const startTime = new Date(today); + startTime.setHours(startHours, startMinutes, 0, 0); + + const endTime = new Date(today); + endTime.setHours(endHours, endMinutes, 0, 0); + + const now = new Date(); + + if (compareAsc(now, startTime) < 0) { + // Not started yet + return 'var(--color-yellow)'; + } else if (compareAsc(now, startTime) >= 0 && compareAsc(now, endTime) <= 0) { + // In progress + return 'var(--color-blue)'; + } else if (compareAsc(now, endTime) > 0) { + // Past due + return '#f23a3ab8'; + } + } catch (error) { + // If time parsing fails, return yellow for "due today" + return 'var(--color-yellow)'; } } else { - return 'var(--color-yellow)'; // Due today but no time info + // Due today but no time info + return 'var(--color-yellow)'; } - } else if (taskUniversalDate > today) { - return 'green'; // Due in future - } else if (taskUniversalDate < today) { - // return 'var(--color-red)'; // Past due - return '#f23a3ab8'; // Past due + } else if (isAfter(taskDateAtStartOfDay, today)) { + // Due in future + return 'green'; + } else if (isBefore(taskDateAtStartOfDay, today)) { + // Past due + return '#f23a3ab8'; } else { - return 'grey'; // No due date + return 'grey'; // No due date or comparison failed } - }, [universalDate, task.time]); + }, [universalDate, task.time, plugin.settings.data.dateFormat]); + // Function to get the card background color based on tags - function getCardBgBasedOnTag(tags: string[]): string | undefined { - if (plugin.settings.data.globalSettings.tagColorsType === "text") { - return undefined; - } + function getCardBgBasedOnTag(): string | undefined { + const allTags = getAllTaskTags(task); + if (globalSettings.tagColorsType === TagColorType.CardBg) { - const tagColors = plugin.settings.data.globalSettings.tagColors; + const tagColors = plugin.settings.data.tagColors; - if (!Array.isArray(tagColors) || tagColors.length === 0) { - return undefined; - } + if (!Array.isArray(tagColors) || tagColors.length === 0) { + return undefined; + } - // Prepare a map for faster lookup - const tagColorMap = new Map(tagColors.map((t) => [t.name, t])); + // Prepare a map for faster lookup + const tagColorMap = new Map(tagColors.map((t) => [t.name, t])); - let highestPriorityTag: { name: string; color: string; priority: number } | undefined = undefined; + let highestPriorityTag: { name: string; color: string; priority: number } | undefined = undefined; - for (const rawTag of tags) { - const tagName = rawTag.replace('#', ''); - let tagData = tagColorMap.get(tagName); + for (const rawTag of allTags) { + const tagName = rawTag.replace('#', ''); + let tagData = tagColorMap.get(tagName); - if (!tagData) { - tagColorMap.forEach((tagColor, tagNameKey, mapValue) => { - const result = matchTagsWithWildcards(tagNameKey, tagName || ''); - // Return the first match found - if (result) tagData = tagColor; - }); - } + if (!tagData) { + tagColorMap.forEach((tagColor, tagNameKey, mapValue) => { + const result = matchTagsWithWildcards(tagNameKey, tagName || ''); + // Return the first match found + if (result) tagData = tagColor; + }); + } - if (tagData) { - if ( - !highestPriorityTag || - (tagData.priority) < (highestPriorityTag.priority) - ) { - highestPriorityTag = tagData; + if (tagData) { + if ( + !highestPriorityTag || + (tagData.priority) < (highestPriorityTag.priority) + ) { + highestPriorityTag = tagData; + } } } - } - const getOpacityValue = (color: string): number => { - const rgbaMatch = color.match(/rgba?\((\d+), (\d+), (\d+)(, (\d+(\.\d+)?))?\)/); - if (rgbaMatch) { - const opacity = rgbaMatch[5] ? parseFloat(rgbaMatch[5]) : 1; - return opacity; - } - return 1; - }; + // const getOpacityValue = (color: string): number => { + // const rgbaMatch = color.match(/rgba?\((\d+), (\d+), (\d+)(, (\d+(\.\d+)?))?\)/); + // if (rgbaMatch) { + // const opacity = rgbaMatch[5] ? parseFloat(rgbaMatch[5]) : 1; + // return opacity; + // } + // return 1; + // }; + + // if (highestPriorityTag && getOpacityValue(highestPriorityTag.color) > 0.2) { + // return updateRGBAOpacity(highestPriorityTag.color, 0.2); + // } - if (highestPriorityTag && getOpacityValue(highestPriorityTag.color) > 0.2) { - return updateRGBAOpacity(plugin, highestPriorityTag.color, 0.2); + return highestPriorityTag?.color; } - return highestPriorityTag?.color; + return undefined; } // ======================================== @@ -517,7 +556,11 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act handleCheckboxChange(plugin, task); } } catch (error) { - console.error("Error updating task:", error); + bugReporterManagerInsatance.addToLogs( + 124, + String(error), + "TaskItemV2.tsx/handleMainCheckBoxClick", + ); } // The component might be unmounted by the time this runs, but this is a safeguard. @@ -554,7 +597,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act // Toggle the checkbox status only for the specific line const symbol = extractCheckboxSymbol(line); - const nextStatus = checkboxStateSwitcher(plugin, symbol); + const nextStatus = checkboxStateSwitcher(globalSettings.customStatuses, symbol); return line.replace(`[${symbol}]`, `[${nextStatus.newSymbol}]`); } @@ -590,7 +633,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act }; const onEditButtonClicked = (event: React.MouseEvent) => { - const settingOption = plugin.settings.data.globalSettings.editButtonAction; + const settingOption = plugin.settings.data.editButtonAction; if (settingOption !== EditButtonMode.NoteInHover) { handleEditTask(plugin, task, settingOption); } else { @@ -601,7 +644,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act } const handleDoubleClickOnCard = (event: React.MouseEvent) => { - const settingOption = plugin.settings.data.globalSettings.doubleClickCardToEdit; + const settingOption = plugin.settings.data.doubleClickCardToEdit; if (settingOption === EditButtonMode.None) return; if (settingOption !== EditButtonMode.NoteInHover) { @@ -622,7 +665,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act return; } - const settingOption = plugin.settings.data.globalSettings.editButtonAction; + const settingOption = plugin.settings.data.editButtonAction; if (settingOption !== EditButtonMode.NoteInHover) { handleEditTask(plugin, childTask, settingOption); } else { @@ -631,13 +674,13 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act event.ctrlKey = false; } } catch (error) { - console.error("Error opening child task modal:", error); bugReporterManagerInsatance.showNotice(12, "Error opening child task modal", String(error), "TaskItem.tsx/handleOpenChildTaskModal"); } } const handleMenuButtonClicked = (event: React.MouseEvent) => { event.stopPropagation(); + const taskItemMenu = new Menu(); taskItemMenu.addItem((item) => { @@ -645,113 +688,182 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act item.setIsLabel(true); }); taskItemMenu.addItem((item) => { - item.setTitle(t("status")); item.setIcon("info"); + item.setTitle(t("status")); const statusMenu = item.setSubmenu() - const customStatues = getCustomStatusOptionsForDropdown(plugin.settings.data.globalSettings.customStatuses); - customStatues.forEach((status) => { + const statusOptions = getCustomStatusOptionsForDropdown( + plugin.settings.data.customStatuses, + { mode: 'flat' } // Context menus don't support optgroups, use flat + ); + + // Handle both output types for future-proofing + const options = statusOptions.type === 'flat' + ? statusOptions.options + : statusOptions.groups.flatMap(group => group.options); + + options.forEach((status) => { statusMenu.addItem((item) => { - item.setTitle(status.text); - // item.setIcon("eye-off"); // TODO : In future map lucude-icons with the ITS theme emoji icons for custom statuses. + // Render status with markdown formatting + MarkdownUIRenderer.renderSubtaskText( + plugin.app, + `- [${status.value}] ${status.label}`, + item.titleEl, + '', + null + ); + item.onClick(() => { - updateTaskItemStatus(plugin, task, status.value); - }) + updateTaskItemStatus(plugin, task, task, status.value); + }); + }); + }); + + // Optional: Add visual separator groups if using grouped mode + if (statusOptions.type === 'grouped') { + // Rebuild menu with group separators (requires clearing and re-adding) + // Note: Obsidian Menu API doesn't natively support optgroups, + // so this is a visual workaround using disabled items + } + }); + + // Priority submenu + taskItemMenu.addItem((item) => { + item.setIcon("flag"); + item.setTitle(t("priority")); + const priMenu = item.setSubmenu(); + const priorityOptions = getPriorityOptionsForDropdown(); + priorityOptions.forEach((p) => { + priMenu.addItem((it) => { + it.setTitle(p.text); + it.onClick(() => updateTaskItemPriority(plugin, task, task, p.value)); }); - }) + }); + }); + + // Tags editor modal - TODO : It doesnt make sense to build another modal specifically changing the tags, when the AddOrEditTaskModal can itself do this. + // taskItemMenu.addItem((item) => { + // item.setTitle(t("tags")); + // item.setIcon("tag"); + // item.onClick(() => { + // const modal = new EditTagsModal(plugin, task.tags || [], (newTags: string[]) => { + // updateTaskItemTags(plugin, task, task, newTags.map((tg) => (tg.startsWith('#') ? tg : `#${tg}`))); + // }); + // modal.open(); + // }); + // }); + + // Dates submenu + + taskItemMenu.addItem((it) => { + it.setIcon("calendar-plus") + it.setTitle(t("start-date")); + it.onClick(async () => { + openDateInputModal(plugin, t("start"), (newDate: string) => { + updateTaskItemDate(plugin, task, task, UniversalDateOptions.startDate, newDate); + }, task.startDate) + }); + }); + taskItemMenu.addItem((it) => { + it.setIcon("calendar-clock") + it.setTitle(t("scheduled-date")); + it.onClick(async () => { + openDateInputModal(plugin, t("scheduled"), (newDate: string) => { + updateTaskItemDate(plugin, task, task, UniversalDateOptions.scheduledDate, newDate); + }, task.scheduledDate) + }); + }); + taskItemMenu.addItem((it) => { + it.setIcon("calendar") + it.setTitle(t("due-date")); + it.onClick(async () => { + openDateInputModal(plugin, t("due"), (newDate: string) => { + updateTaskItemDate(plugin, task, task, UniversalDateOptions.dueDate, newDate); + }, task.due) + }); + }); + + // Reminder item - open prompt for date/time + taskItemMenu.addItem((item) => { + item.setIcon("clock"); + item.setTitle(t("reminder")); + item.onClick(async () => { + const modal = new DateTimePickerModal(plugin, t("reminder"), task.reminder); + modal.onDateTimeSelected = (dateTime) => { // e.g., "2024-01-15T14:30" or "14:30" + updateTaskItemReminder(plugin, task, task, dateTime); + }; + modal.open(); + }); }); taskItemMenu.addSeparator(); taskItemMenu.addItem((item) => { - item.setTitle(t("quick-actions")); + item.setTitle(t("task-actions")); item.setIsLabel(true); }); taskItemMenu.addItem((item) => { + item.setIcon("copy"); item.setTitle(t("copy-task-title")); - item.setIcon("eye-off"); item.onClick(async () => { + try { + await navigator.clipboard.writeText(cleanTaskTitleLegacy(task)); + new Notice(t("copy-task-title-successful")); + } catch (error) { + new Notice(t("copy-task-title-unsuccessful")); + } }); - - // Priority submenu - taskItemMenu.addItem((item) => { - item.setTitle(t("priority")); - item.setIcon("flag"); - const priMenu = item.setSubmenu(); - const priorities = [ - { label: t("none"), value: 0 }, - { label: t("low"), value: 1 }, - { label: t("medium"), value: 2 }, - { label: t("high"), value: 3 }, - ]; - priorities.forEach((p) => { - priMenu.addItem((it) => { - it.setTitle(p.label); - it.onClick(() => updateTaskItemPriority(plugin, task, p.value)); - }); - }); + }); + taskItemMenu.addItem((item) => { + item.setIcon("square-pen"); + item.setTitle(t("open-task-editor")); + item.onClick(async () => { + handleEditTask(plugin, task, EditButtonMode.Modal); }); - - // Dates submenu - taskItemMenu.addItem((item) => { - item.setTitle(t("dates")); - item.setIcon("calendar"); - const dateMenu = item.setSubmenu(); - dateMenu.addItem((it) => { - it.setTitle(t("start-date")); - it.onClick(async () => { - const newDate = await showTextInputModal(plugin.app, { title: t("set-start-date"), placeholder: 'YYYY-MM-DD', initialValue: task.startDate || '' }); - if (newDate) updateTaskItemDate(plugin, task, 'startDate', newDate); - }); - }); - dateMenu.addItem((it) => { - it.setTitle(t("scheduled-date")); - it.onClick(async () => { - const newDate = await showTextInputModal(plugin.app, { title: t("set-scheduled-date"), placeholder: 'YYYY-MM-DD', initialValue: task.scheduledDate || '' }); - if (newDate) updateTaskItemDate(plugin, task, 'scheduledDate', newDate); - }); - }); - dateMenu.addItem((it) => { - it.setTitle(t("due-date")); - it.onClick(async () => { - const newDate = await showTextInputModal(plugin.app, { title: t("set-due-date"), placeholder: 'YYYY-MM-DD', initialValue: task.due || '' }); - if (newDate) updateTaskItemDate(plugin, task, 'due', newDate); - }); - }); + }); + taskItemMenu.addItem((item) => { + item.setIcon("square-pen"); + item.setTitle(t("open-task-editor-in")); + const taskEditorMenu = item.setSubmenu(); + taskEditorMenu.addItem((subItem) => { + subItem.setIcon("columns-2"); + subItem.setTitle(t("right-split")); + subItem.onClick(() => handleEditTask(plugin, task, EditButtonMode.ViewInSplitTab)); }); - // Reminder item - open prompt for date/time - taskItemMenu.addItem((item) => { - item.setTitle(t("reminder")); - item.setIcon("clock"); - item.onClick(async () => { - const newReminder = await showTextInputModal(plugin.app, { title: t("set-reminder"), placeholder: 'YYYY-MM-DD HH:mm', initialValue: task.reminder || '' }); - if (newReminder) updateTaskItemReminder(plugin, task, newReminder); - }); + taskEditorMenu.addItem((subItem) => { + subItem.setIcon("picture-in-picture-2"); + subItem.setTitle(t("new-window")); + subItem.onClick(() => handleEditTask(plugin, task, EditButtonMode.ViewInWindow)); }); + }); - // Tags editor modal - taskItemMenu.addItem((item) => { - item.setTitle(t("tags")); - item.setIcon("tag"); - item.onClick(() => { - const modal = new EditTagsModal(plugin, task.tags || [], (newTags: string[]) => { - updateTaskItemTags(plugin, task, task, newTags.map((tg) => (tg.startsWith('#') ? tg : `#${tg}`))); - }); - modal.open(); - }); - }); + taskItemMenu.addSeparator(); + + taskItemMenu.addItem((item) => { + item.setTitle(t("note-actions")); + item.setIsLabel(true); }); + taskItemMenu.addItem((item) => { + item.setIcon("file-input"); item.setTitle(t("open-note")); - item.setIcon("eye-off"); item.onClick(async () => { + handleEditTask(plugin, task, EditButtonMode.NoteInTab) + }); + }); + taskItemMenu.addItem((item) => { + item.setIcon("columns-2"); + item.setTitle(t("open-note-to-right")); + item.onClick(async () => { + handleEditTask(plugin, task, EditButtonMode.NoteInSplit) }); }); + // Note actions submenu taskItemMenu.addItem((item) => { - item.setTitle(t("contextMenus.task.noteActions")); item.setIcon("file-text"); + item.setTitle(t("more-note-actions")); const submenu = (item as any).setSubmenu(); @@ -767,16 +879,16 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act } // Add common file actions (these will either supplement or replace the native menu) - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.rename")); + submenu.addItem((subItem: MenuItem) => { subItem.setIcon("pencil"); + subItem.setTitle(t("rename-note")); subItem.onClick(async () => { try { // Modal-based rename const currentName = file.basename; const newName = await showTextInputModal(plugin.app, { - title: t("contextMenus.task.renameTitle"), - placeholder: t("contextMenus.task.renamePlaceholder"), + title: t("rename-note"), + placeholder: t("rename-note-placeholder"), initialValue: currentName, }); @@ -794,115 +906,32 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act // Rename the file await plugin.app.vault.rename(file, newPath); - new Notice( - t("contextMenus.task.notices.renameSuccess") + finalName, - - ); - - // // Trigger update callback - // if (options.onUpdate) { - // options.onUpdate(); - // } + new Notice("File renamed successfully."); } } catch (error) { - console.error("Error renaming file:", error); - new Notice(t("contextMenus.task.notices.renameFailure")); + new Notice("There was an error while renaming the file."); + bugReporterManagerInsatance.addToLogs( + 125, + String(error), + "TaskItem.tsx/handleMenuButtonClicked/renaming", + ); } }); }); - submenu.addItem((subItem: any) => { - subItem.setTitle(t("delete-note")); + submenu.addItem((subItem: MenuItem) => { subItem.setIcon("trash"); + subItem.setTitle(t("delete-note")); subItem.onClick(async () => { - // Show confirmation and delete - const confirmed = await showConfirmationModal(plugin.app, { - title: t("contextMenus.task.deleteTitle"), - message: t("contextMenus.task.deleteMessage") + file.name, - confirmText: t("contextMenus.task.deleteConfirm"), - cancelText: t("common.cancel"), - isDestructive: true, - }); - if (confirmed) { - plugin.app.vault.trash(file, true); - } - }); - }); - - submenu.addSeparator(); - - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.copyPath")); - subItem.setIcon("copy"); - subItem.onClick(async () => { - try { - await navigator.clipboard.writeText(file.path); - new Notice(t("contextMenus.task.notices.copyPathSuccess")); - } catch (error) { - new Notice(t("contextMenus.task.notices.copyFailure")); - } - }); - }); - - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.copyUrl")); - subItem.setIcon("link"); - subItem.onClick(async () => { - try { - const url = `obsidian://open?vault=${encodeURIComponent(plugin.app.vault.getName())}&file=${encodeURIComponent(file.path)}`; - await navigator.clipboard.writeText(url); - new Notice(t("contextMenus.task.notices.copyUrlSuccess")); - } catch (error) { - new Notice(t("contextMenus.task.notices.copyFailure")); - } - }); - }); - - submenu.addSeparator(); - - submenu.addItem((subItem: any) => { - subItem.setTitle(t("contextMenus.task.showInExplorer")); - subItem.setIcon("folder-open"); - subItem.onClick(() => { - // Reveal file in file explorer - plugin.app.workspace - .getLeaf() - .setViewState({ - type: "file-explorer", - state: {}, - }) - .then(() => { - // Focus the file in the explorer - const fileExplorer = - plugin.app.workspace.getLeavesOfType("file-explorer")[0]; - if (fileExplorer?.view && "revealInFolder" in fileExplorer.view) { - (fileExplorer.view as any).revealInFolder(file); - } - }); + plugin.app.vault.trash(file, true).then(() => { + new Notice("File deleted successfully. Moved to system trash."); + }) + // handleDeleteTask(plugin, task, true); }); }); } }); - // // Show minimize or maximize option based on current state - // if (columnData.minimized) { - // taskItemMenu.addItem((item) => { - // item.setTitle(t("maximize-column")); - // item.setIcon("panel-left-open"); - // item.onClick(async () => { - // await handleMinimizeColumn(); - // }); - // }); - // } else { - // taskItemMenu.addItem((item) => { - // item.setTitle(t("minimize-column")); - // item.setIcon("panel-left-close"); - // item.onClick(async () => { - // await handleMinimizeColumn(); - // }); - // }); - // } - // Use native event if available (React event has nativeEvent property) taskItemMenu.showAtMouseEvent( (event instanceof MouseEvent ? event : event.nativeEvent) @@ -911,59 +940,41 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act // Handlers for drag and drop const handleDragStart = useCallback((e: React.DragEvent) => { - console.log("TaskItem : handleDragStart..."); + // prevent column drag from also starting + e.stopPropagation(); + if (!columnData) { e.preventDefault(); - console.warn('handleDragStart: columnData is undefined'); + bugReporterManagerInsatance.addToLogs(91, `Column data : undefined`, "TaskItem.tsx/handleDragStart"); return; } - - setIsDragging(true); - // Delegate to manager for standardized behavior (sets current payload and dims element) try { const el = taskItemRef.current as HTMLDivElement; - const payload: currentDragDataPayload = { task, taskIndex: String(dataAttributeIndex), sourceColumnData: columnData, currentBoardIndex: activeBoardSettings.index, swimlaneData: swimlaneData }; - dragDropTasksManagerInsatance.handleDragStartEvent(e.nativeEvent as DragEvent, el, payload, 0); - - // Add dragging class after a small delay to not affect the drag image - const clone = el.cloneNode(true) as HTMLDivElement; - e.dataTransfer?.setDragImage(el, 0, 0); - requestAnimationFrame(() => { - clone.classList.add("task-item-dragging"); - console.log("TaskItem : handleDragStart... done : ", el); - }); - - // Also set a drag image from the whole task element so the preview is the full card - // TODO : The drag image is taking too much width and also its still in its default state, like very dimmed opacity. Improve it to get a nice border and increase the opacity so it looks more real. - // if (taskItemRef.current && e.dataTransfer) { - // console.log("TaskItemRef.current", taskItemRef.current); - // const clone = taskItemRef.current.cloneNode(true) as HTMLElement; - // // clone.style.boxShadow = '0 8px 16px rgba(0,0,0,0.12)'; - // clone.style.opacity = '0.5'; - // clone.style.position = 'absolute'; - // // clone.style.top = '-9999px'; - // // document.body.appendChild(clone); - // const rect = taskItemRef.current.getBoundingClientRect(); - // e.dataTransfer.setDragImage(clone, rect.width, rect.height); - // setTimeout(() => { - // try { document.body.removeChild(clone); } catch { } - // }, 0); - // } + const payload: currentDragDataPayload = { + task, + taskIndex: String(dataAttributeIndex), + sourceColumnData: columnData, + currentViewIndex: activeViewIndex, + currentBoardID: activeBoardID, + swimlaneData: swimlaneData + }; + // Delegate to manager for standardized behavior (sets current payload and dims element) + dragDropTasksManagerInsatance.handleDragStartEvent(e.nativeEvent as DragEvent, el, payload); } catch (err) { // fallback minimal behavior // try { // e.dataTransfer.setData('application/json', JSON.stringify({ task, sourceColumnData: columnData })); // e.dataTransfer.effectAllowed = 'move'; // } catch (ex) {/* ignore */ } - - console.error(err); + bugReporterManagerInsatance.addToLogs( + 126, + String(err), + "TaskItem.tsx/handleDragStart", + ); } }, [task, columnData]); const handleDragEnd = useCallback(() => { - console.log("TaskItem : handleDragEnd..."); - setIsDragging(false); - // Remove dim effect from this dragged task and clear manager state if (taskItemRef.current) { dragDropTasksManagerInsatance.removeDimFromDraggedTaskItem(taskItemRef.current); @@ -982,85 +993,96 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act try { return (
-
- {/* Render priority */} - {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Priority) && task.priority > 0 && ( -
-
{task.priority} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.FilePathInHeader) && task.filePath && ( +
+
{task.filePath.split('/').pop()}
+
+ )} + +
+
+ {/* Render priority */} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Priority) && task.priority > 0 && ( +
+
{task.priority} +
-
- )} + )} - {/* Render tags individually */} - {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Tags) && task.tags.length > 0 && ( -
- {/* Render line tags (editable) */} - {task.tags.map((tag: string) => { - const tagName = tag.replace('#', ''); - const customTag = plugin.settings.data.globalSettings.tagColorsType === "text" ? plugin.settings.data.globalSettings.tagColors.find(t => t.name === tagName) : undefined; - const tagColor = customTag?.color || null; - // const backgroundColor = customTag ? updateRGBAOpacity(plugin, customTag.color, 0.1) : `var(--tag-background)`; // 10% opacity background - // const borderColor = customTag ? updateRGBAOpacity(plugin, customTag.color, 0.5) : `var(--tag-color-hover)`; - - // If columnIndex is defined, proceed to get the column - if ( - (!activeBoardSettings?.showColumnTags) && - columnData && - columnData?.colType === colTypeNames.namedTag && - tagName.replace('#', '') === columnData?.coltag?.replace('#', '') - ) { - return null; - } - - const tagKey = `${task.id}-${tag}`; - // Render the remaining tags - return ( -
- {tag} -
- ); - })} - - {/* Render frontmatter tags (read-only) */} - {task.frontmatterTags && task.frontmatterTags.map((tag: string) => { - const tagKey = `${task.id}-fm-${tag}`; - // Render frontmatter tags with different styling - return ( -
- {tag} -
- ); - })} -
- )} -
+ {/* Render tags individually */} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Tags) && (task.tags.length > 0 || task.frontmatterTags.length > 0) && ( +
+ {/* Render line tags (editable) */} + {task.tags.map((tag: string) => { + const isTagBg = globalSettings.tagColorsType === TagColorType.TagBg; + const isCardBg = globalSettings.tagColorsType === TagColorType.CardBg; + const taskTag = tag.replace('#', '').toLowerCase(); + const columnTag = columnData?.coltag?.replace('#', '').toLowerCase(); + + const customTag = isCardBg ? undefined : plugin.settings.data.tagColors.find(t => t.name.replace('#', '').toLowerCase() === taskTag); + + const tagColor = customTag?.color; + const dimmedTagColor = customTag ? updateRGBAOpacity(customTag.color, 0.1) : undefined; // 10% opacity background + // const borderColor = customTag ? updateRGBAOpacity(customTag.color, 0.5) : `var(--tag-color-hover)`; + + // If columnIndex is defined, proceed to get the column + if ( + activeViewType === viewTypeNames.kanban && + kanbanViewData && + kanbanViewData.showColumnTags && + columnData && + columnData?.colType === colTypeNames.namedTag && + taskTag === columnTag + ) { + return null; + } -
- {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ID) && task.legacyId && ( -
-
ID
{task.legacyId}
-
- )} + const tagKey = `${task.id}-${tag}`; + // Render the remaining tags + return ( +
+ {tag} +
+ ); + })} + + {/* Render frontmatter tags (read-only) */} + {task.frontmatterTags && task.frontmatterTags.map((tag: string) => { + const tagKey = `${task.id}-fm-${tag}`; + // Render frontmatter tags with different styling + return ( +
+ {tag} +
+ ); + })} +
+ )} +
+
+ {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ID) && task.legacyId && ( +
+
ID
{task.legacyId}
+
+ )} +
); } catch (error) { - // bugReporterManagerInsatance.showNotice(13, "Error while rendering task header", error as string, "TaskItem.tsx/renderHeader"); - console.warn("TaskItem.tsx/renderHeader : Error while rendering task header", error); + bugReporterManagerInsatance.addToLogs(13, error as string, "TaskItemV2.tsx/renderHeader"); return null; } }; @@ -1079,7 +1101,6 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act const completed = allSubTasks.filter(line => isTaskCompleted(line, false, plugin.settings)).length; const showSubTaskSummaryBar = globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.SubTasksMinimized); - console.log("Show subtasks :", showSubtasks, "\nShow subtasks summary :", showSubTaskSummaryBar); return ( <> @@ -1120,7 +1141,8 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act const tabMatchInTitle = task.title.match(new RegExp(`^(${tabString})+`)); const titleTabs = tabMatchInTitle && tabMatchInTitle[0] ? tabMatchInTitle[0].length / tabString.length : 0; const numTabs = tabMatch && tabMatch[0] ? tabMatch[0].length / tabString.length : 0; - const paddingLeft = numTabs > 1 ? `${(numTabs - titleTabs - 1) * 15}px` : '0px'; + const numOfTabs = isTaskNote ? numTabs + 1 : numTabs; + const paddingLeft = numOfTabs > 1 ? `${(numOfTabs - titleTabs - 1) * 15}px` : '0px'; // Create a unique key for this subtask based on task.id and index const uniqueKey = `${task.id}-${index}`; @@ -1129,7 +1151,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act
= ({ dataAttributeIndex, plugin, task, act ); } catch (error) { - // bugReporterManagerInsatance.showNotice(14, "Error while rendering sub-tasks", error as string, "TaskItem.tsx/renderSubTasks"); - console.warn("TaskItem.tsx/renderSubTasks : Error while rendering sub-tasks", error); + bugReporterManagerInsatance.addToLogs(14, error as string, "TaskItemV2.tsx/renderSubTasks"); return null; } }; @@ -1180,7 +1201,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Status) && task?.status && (
{t("status")}
-
{getStatusNameFromStatusSymbol(task.status, globalSettings)}
+
{getStatusNameFromStatusSymbol(task.status, globalSettings.customStatuses ?? [])}
)} {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.Time) && task?.time && ( @@ -1234,7 +1255,19 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.FilePath) && task.filePath && (
{t("file")}
-
{task.filePath}
+
{task.filePath.split('/').pop()}
+
+ )} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.ParentFolder) && task.filePath && ( +
+
{t("folder")}
+
{task.filePath.split('/')[task.filePath.split('/').length - 2] ? task.filePath.split('/')[task.filePath.split('/').length - 2] : "Vault root"}
+
+ )} + {globalSettings.visiblePropertiesList?.includes(taskPropertiesNames.FullPath) && task.filePath && ( +
+
{t("path")}
+
{task.filePath.split('/').slice(0, -1).join("/") ? task.filePath.split('/').slice(0, -1).join("/") : "Vault root"}
)}
@@ -1243,8 +1276,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act ); } catch (error) { - // bugReporterManagerInsatance.showNotice(15, "Error while rendering task footer", error as string, "TaskItem.tsx/renderFooter"); - console.warn("TaskItem.tsx/renderFooter : Error while rendering task footer", error); + bugReporterManagerInsatance.addToLogs(15, error as string, "TaskItemV2.tsx/renderFooter"); return null; } }; @@ -1272,7 +1304,7 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act const renderChildTasks = () => { try { // Render only if the last viewed history is Kanban and there are child tasks - if (plugin.settings.data.globalSettings.lastViewHistory.viewedType === viewTypeNames.kanban && task?.dependsOn && task.dependsOn.length > 0) { + if (activeViewType === viewTypeNames.kanban && task?.dependsOn && task.dependsOn.length > 0) { return (
{/* Placeholder for future child tasks rendering */} @@ -1306,17 +1338,16 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act return null; } } catch (error) { - // bugReporterManagerInsatance.showNotice(16, "Error while rendering child-tasks", error as string, "TaskItem.tsx/renderChildTasks"); - console.warn("TaskItem.tsx/renderChildTasks : Error while rendering child-tasks", error); + bugReporterManagerInsatance.addToLogs(16, error as string, "TaskItemV2.tsx/renderChildTasks"); return null; } }; // Memoize the render functions to prevent unnecessary re-renders - const memoizedRenderHeader = useMemo(() => renderHeader(), [plugin.settings.data.globalSettings.visiblePropertiesList, task.priority, task.tags, activeBoardSettings]); - const memoizedRenderSubTasks = useMemo(() => renderSubTasks(), [plugin.settings.data.globalSettings.visiblePropertiesList, task.body, showSubtasks]); + const memoizedRenderHeader = useMemo(() => renderHeader(), [plugin.settings.data.visiblePropertiesList, task.priority, task.tags, columnData]); + const memoizedRenderSubTasks = useMemo(() => renderSubTasks(), [plugin.settings.data.visiblePropertiesList, task.body, showSubtasks]); const memoizedRenderChildTasks = useMemo(() => renderChildTasks(), [task.dependsOn, childTasksData]); - // const memoizedRenderFooter = useMemo(() => renderFooter(), [plugin.settings.data.globalSettings.showFooter, task.completion, universalDate, task.time]); + // const memoizedRenderFooter = useMemo(() => renderFooter(), [plugin.settings.data.showFooter, task.completion, universalDate, task.time]); // ======================================== // RETURN STATEMENT (UPDATED) @@ -1325,11 +1356,14 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act
@@ -1337,31 +1371,15 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act {memoizedRenderHeader} {/* Drag Handle and Task Menu button */} - {plugin.settings.data.globalSettings.experimentalFeatures && ( - <> - { - Platform.isPhone ? ( - <> -
- - ) : ( - <> - {/* Drag Handle */} - {columnData?.colType !== colTypeNames.allPending && plugin.settings.data.globalSettings.lastViewHistory.viewedType === viewTypeNames.kanban && ( -
- -
- )} - - ) - } - - )} + { + Platform.isDesktopApp && activeViewType !== viewTypeNames.map ? ( +
+ +
+ ) : ( +
+ ) + } {/* Task Content */}
@@ -1370,9 +1388,9 @@ const TaskItemV2: React.FC = ({ dataAttributeIndex, plugin, task, act { diff --git a/src/editor-extensions/task-operations/gutter-marker.ts b/src/editor-extensions/task-operations/gutter-marker.ts index 2d9afa95..cbcb3398 100644 --- a/src/editor-extensions/task-operations/gutter-marker.ts +++ b/src/editor-extensions/task-operations/gutter-marker.ts @@ -6,10 +6,10 @@ import { EditorView, gutter, GutterMarker } from "@codemirror/view"; import { Extension } from "@codemirror/state"; import { App, ExtraButtonComponent } from "obsidian"; -import TaskBoard from "main"; -import { isTaskLine } from "src/utils/CheckBoxUtils"; import { syntaxTree, tokenClassNodeProp } from "@codemirror/language"; -import { AddOrEditTaskModal } from "src/modals/AddOrEditTaskModal"; +import TaskBoard from "../../../main.js"; +import { AddOrEditTaskModal } from "../../modals/AddOrEditTaskModal.js"; +import { isTaskLine } from "../../utils/CheckBoxUtils.js"; // Task icon marker class TaskGutterMarker extends GutterMarker { diff --git a/src/editor-extensions/task-operations/property-hiding.ts b/src/editor-extensions/task-operations/property-hiding.ts index dd1fa322..20eae6a7 100644 --- a/src/editor-extensions/task-operations/property-hiding.ts +++ b/src/editor-extensions/task-operations/property-hiding.ts @@ -3,7 +3,6 @@ * Properties are hidden by default and revealed when the cursor is positioned on them. */ -import type TaskBoard from "main"; import { EditorView, Decoration, @@ -14,13 +13,11 @@ import { } from "@codemirror/view"; import { Extension, Range, StateField } from "@codemirror/state"; import { syntaxTree, tokenClassNodeProp } from "@codemirror/language"; -import { isTaskLine } from "src/utils/CheckBoxUtils"; -import { - TaskRegularExpressions, - TASKS_PLUGIN_DEFAULT_SYMBOLS, -} from "src/regularExpressions/TasksPluginRegularExpr"; -import { DATAVIEW_PLUGIN_DEFAULT_SYMBOLS } from "src/regularExpressions/DataviewPluginRegularExpr"; -import { taskPropertiesNames } from "src/interfaces/Enums"; +import TaskBoard from "../../../main.js"; +import { taskPropertiesNames } from "../../interfaces/Enums.js"; +import { DATAVIEW_PLUGIN_DEFAULT_SYMBOLS } from "../../regularExpressions/DataviewPluginRegularExpr.js"; +import { TASKS_PLUGIN_DEFAULT_SYMBOLS, TaskRegularExpressions } from "../../regularExpressions/TasksPluginRegularExpr.js"; +import { isTaskLine } from "../../utils/CheckBoxUtils.js"; /** * Widget for showing placeholder text when properties are hidden @@ -239,7 +236,7 @@ function createPropertyDecorations( ): DecorationSet { const decorations: Range[] = []; const hiddenProperties = - plugin.settings.data.globalSettings.hiddenTaskProperties || []; + plugin.settings.data.hiddenTaskProperties || []; const cursorPos = view.state.selection.main.head; const doc = view.state.doc; @@ -274,7 +271,7 @@ function createPropertyDecorations( hiddenProperties.forEach((property) => { const pattern = getTaskPropertyRegexPatterns( property, - plugin.settings.data.globalSettings?.taskPropertyFormat + plugin.settings.data?.taskPropertyFormat ); const matches = Array.from(lineText.matchAll(pattern)); // console.log( diff --git a/src/interfaces/BoardConfigs.ts b/src/interfaces/BoardConfigs.ts index 4ea22bdd..8e334791 100644 --- a/src/interfaces/BoardConfigs.ts +++ b/src/interfaces/BoardConfigs.ts @@ -1,3 +1,11 @@ +import { + viewTypeNames, + colTypeNames, + HeaderUITypeOptions, + defaultTaskStatuses, + viewsPanelPropertiesToShow, +} from "./Enums.js"; + export interface columnSortingCriteria { criteria: | "status" @@ -67,6 +75,7 @@ export type ColumnData = { active: boolean; collapsed?: boolean; minimized?: boolean; + swimlaneEnabled?: boolean; name: string; coltag?: string; filePaths?: string; @@ -108,7 +117,7 @@ export interface FilterConfigSettings { savedConfigs: SavedFilterConfig[]; } -export interface swimlaneConfigs { +export type swimlaneConfigs = { enabled: boolean; hideEmptySwimlanes: boolean; maxHeight: string; @@ -120,28 +129,389 @@ export interface swimlaneConfigs { index: number; }[]; // This is only if user selects "custom" as the sort criteria. groupAllRest?: boolean; // This will be only visible for customSortOrder. It will help user to decide if they want to group all the rest of the task below the custom sort order. - verticalHeaderUI: boolean; // This is a temporary setting for user telemetry. Later will remove it based on user feedback. + headerUIType: string; minimized: string[]; // This will store the names of the minimized swimlanes. +}; + +export type viewPortType = { + x: number; + y: number; + zoom: number; +}; + +export type nodePositionData = { + x: number; + y: number; + width?: number; +}; + +export type nodeDataType = { + [taskID: string]: nodePositionData; +}; + +export interface MapView { + viewPortData: viewPortType; + nodesData: nodeDataType; } -export type Board = { - name: string; - description?: string; - index: number; +export interface KanbanView { columns: ColumnData[]; - hideEmptyColumns: boolean; showColumnTags: boolean; + hideEmptyColumns: boolean; + swimlanes: swimlaneConfigs; +} + +/** + * Interface for the Task Board view. It will store the data specific to a particular view created by user inside the board. + */ +export interface TaskBoardViewType { + viewId: string; + viewName: string; + viewType: string; + description?: string; showFilteredTags: boolean; - boardFilter: RootFilterState; - filterConfig?: FilterConfigSettings; - taskCount?: { + viewFilter: RootFilterState; + taskCount: { pending: number; completed: number; }; - swimlanes: swimlaneConfigs; + + // All configurations specific to the kanban view + kanbanView?: KanbanView; + + // All configurations specific to the map view + mapView?: MapView; + + // More views will be added in the future +} + +export type Board = { + id: string; + /** + * This property will help us to manage the migrations in future when we will be adding + * new properties to the board or view data structure. Whenever there will be a breaking + * change in the data structure, we will update this revision and during the loading of + * the board data, we can check this revision number and can decide if we need to run + * specific selective migration functions to update the data structure to the latest one. + */ + revision: number; + name: string; + description?: string; + filterConfig?: FilterConfigSettings; + + views: TaskBoardViewType[]; + lastViewId: string; + viewsPanel: { + isOpen: boolean; + width: number; + propertiesToShow: string[]; + buttonsBelt: boolean; + }; + // TODO : Below two settings has been deprecated since version `1.8.0`. Only kept here because of migrations. Remove it while removing the migrations. filters?: string[]; filterPolarity?: string; + pluginVersion?: string; }; -export type BoardConfigs = Board[]; +// A single board is a single project, inside a board user will create multiple types of views to visualize their tasks in different ways. Hence, when user will install this plugin for the first time, will only going to have a single board to which will be enought show the capabilities of this plugin and later user can easily create more boards. +export const DEFAULT_BOARD: Board = { + id: "3103563481", + revision: 0, + name: "My Project", + description: + "This is my personal project. This is a default board created by Task Board for you to kick start your journey with Task Board. Feel free to edit or create new boards.", + lastViewId: "3103563482", + views: [ + { + viewId: "3103563482", + viewName: "Time Based Workflow", + viewType: viewTypeNames.kanban, + showFilteredTags: true, + viewFilter: { + rootCondition: "any", + filterGroups: [], + }, + taskCount: { + pending: 0, + completed: 0, + }, + kanbanView: { + columns: [ + { + id: 3103563491, + colType: colTypeNames.undated, + active: true, + collapsed: false, + name: "Undated Tasks", + index: 1, + datedBasedColumn: { + dateType: "due", + from: 0, + to: 0, + }, + }, + { + id: 3103563492, + colType: colTypeNames.dated, + active: true, + collapsed: false, + name: "Over Due", + index: 2, + datedBasedColumn: { + dateType: "due", + from: -300, + to: -1, + }, + }, + { + id: 3103563493, + colType: colTypeNames.dated, + active: true, + collapsed: false, + name: "Today", + index: 3, + datedBasedColumn: { + dateType: "due", + from: 0, + to: 0, + }, + }, + { + id: 3103563494, + colType: colTypeNames.dated, + active: true, + collapsed: false, + name: "Tomorrow", + index: 4, + datedBasedColumn: { + dateType: "due", + from: 1, + to: 1, + }, + }, + { + id: 3103563495, + colType: colTypeNames.dated, + active: true, + collapsed: false, + name: "Future", + index: 5, + datedBasedColumn: { + dateType: "due", + from: 2, + to: 300, + }, + }, + { + id: 3103563496, + colType: colTypeNames.completed, + active: true, + collapsed: false, + limit: 20, + name: "Completed", + index: 6, + }, + ], + showColumnTags: false, + hideEmptyColumns: false, + swimlanes: { + enabled: false, + hideEmptySwimlanes: false, + property: "tags", + sortCriteria: "asc", + minimized: [], + maxHeight: "300px", + headerUIType: HeaderUITypeOptions.horizontal, + }, + }, + }, + { + viewId: "3103563483", + viewName: "Tag Based Workflow", + viewType: viewTypeNames.kanban, + showFilteredTags: true, + viewFilter: { + rootCondition: "any", + filterGroups: [], + }, + taskCount: { + pending: 0, + completed: 0, + }, + kanbanView: { + columns: [ + { + id: 3103563497, + colType: colTypeNames.untagged, + active: true, + collapsed: false, + name: "Backlogs", + index: 1, + }, + { + id: 3103563498, + colType: colTypeNames.namedTag, + active: true, + collapsed: false, + name: "Important", + index: 2, + coltag: "important", + }, + { + id: 3103563499, + colType: colTypeNames.namedTag, + active: true, + collapsed: false, + name: "WIP", + index: 3, + coltag: "wip", + }, + { + id: 3103563500, + colType: colTypeNames.namedTag, + active: true, + collapsed: false, + name: "In Review", + index: 5, + coltag: "review", + }, + { + id: 3103563501, + colType: colTypeNames.completed, + active: true, + collapsed: false, + index: 6, + limit: 20, + name: "Completed", + }, + ], + showColumnTags: false, + hideEmptyColumns: false, + swimlanes: { + enabled: false, + hideEmptySwimlanes: false, + property: "tags", + sortCriteria: "asc", + minimized: [], + maxHeight: "300px", + headerUIType: HeaderUITypeOptions.horizontal, + }, + }, + }, + { + viewId: "3103563484", + viewName: "Status Based Workflow", + viewType: viewTypeNames.kanban, + showFilteredTags: true, + viewFilter: { + rootCondition: "any", + filterGroups: [], + }, + taskCount: { + pending: 0, + completed: 0, + }, + kanbanView: { + columns: [ + { + id: 3103563502, + colType: colTypeNames.taskStatus, + taskStatus: defaultTaskStatuses.unchecked, + active: true, + collapsed: false, + name: "Backlogs", + index: 1, + }, + { + id: 3103563503, + colType: colTypeNames.taskStatus, + taskStatus: defaultTaskStatuses.scheduled, + active: true, + collapsed: false, + name: "Ready to start", + index: 2, + }, + { + id: 3103563504, + colType: colTypeNames.taskStatus, + taskStatus: defaultTaskStatuses.inprogress, + active: true, + collapsed: false, + name: "In Progress", + index: 3, + }, + { + id: 3103563505, + colType: colTypeNames.taskStatus, + taskStatus: defaultTaskStatuses.question, + active: true, + collapsed: false, + name: "In Review", + index: 5, + }, + { + id: 3103563506, + colType: colTypeNames.completed, + active: true, + collapsed: false, + index: 6, + limit: 20, + name: "Completed", + }, + { + id: 3103563507, + colType: colTypeNames.taskStatus, + taskStatus: defaultTaskStatuses.dropped, + active: true, + collapsed: false, + name: "Cancelled", + index: 7, + }, + ], + showColumnTags: false, + hideEmptyColumns: false, + swimlanes: { + enabled: false, + hideEmptySwimlanes: false, + property: "tags", + sortCriteria: "asc", + minimized: [], + maxHeight: "300px", + headerUIType: HeaderUITypeOptions.horizontal, + }, + }, + }, + { + viewId: "3103563485", + viewName: "Map View", + viewType: viewTypeNames.map, + showFilteredTags: true, + viewFilter: { + rootCondition: "any", + filterGroups: [], + }, + taskCount: { + pending: 0, + completed: 0, + }, + mapView: { + viewPortData: { + x: 0, + y: 0, + zoom: 1, + }, + nodesData: {}, + }, + }, + ], + viewsPanel: { + isOpen: true, + width: 300, + propertiesToShow: [ + viewsPanelPropertiesToShow.Title, + viewsPanelPropertiesToShow.Description, + ], + buttonsBelt: true, + }, +}; diff --git a/src/interfaces/Constants.ts b/src/interfaces/Constants.ts index a65cc030..decf5fa9 100644 --- a/src/interfaces/Constants.ts +++ b/src/interfaces/Constants.ts @@ -1,7 +1,9 @@ -export const newReleaseVersion = "1.8.7"; +export const CURRENT_PLUGIN_VERSION = "2.0.0-beta-1"; +export const CURRENT_REVISION = 0; // Plugin view type identifiers export const VIEW_TYPE_TASKBOARD = "task-board-view"; export const VIEW_TYPE_ADD_OR_EDIT_TASK = "add-or-edit-task-view"; +export const TASKBOARD_FILE_EXTENSION = "taskboard"; // Local storage keys // const LOCAL_STORAGE_KEY = "taskBoardCachedLang"; @@ -10,9 +12,16 @@ export const PENDING_SCAN_FILE_STACK = "taskBoard_file_stack"; export const NODE_POSITIONS_STORAGE_KEY = "taskboard_map_node_positions"; // now stores board-wise export const NODE_SIZE_STORAGE_KEY = "taskboard_map_node_sizes"; export const VIEWPORT_STORAGE_KEY = "taskboard_map_viewport"; +export const OBSIDIAN_CLOSED_TIME_KEY = "OBSIDIAN_CLOSED_TIME"; +export const LEAFID_FILEPATH_MAPPING_KEY = "taskboard_leafid_filepath_map"; +export const MANDATORY_SCAN_KEY = "taskboard_manadatoryScan"; // Default file names and paths export const DEFAULT_TASKS_CACHE_FILE = "task-board-data.json"; export const DEFAULT_ARCHIVED_TASKS_FILE = "archived-tasks.json"; export const DEFAULT_PREDEFINED_NOTE = "Task_board_note.md"; export const DEFAULT_TASKS_FOLDER = "TaskNotes"; + +export const DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; +export const DEFAULT_TIME_FORMAT = "HH:mm:ss"; +export const DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; // This project uses `date-fns` library, do not change this format. diff --git a/src/interfaces/Enums.ts b/src/interfaces/Enums.ts index 8460b2dc..6a1467e5 100644 --- a/src/interfaces/Enums.ts +++ b/src/interfaces/Enums.ts @@ -8,7 +8,8 @@ export enum taskPropertyFormatOptions { export enum EditButtonMode { None = "none", Modal = "popUp", - View = "view", + ViewInSplitTab = "viewInSplit", + ViewInWindow = "viewInWindow", TasksPluginModal = "tasksPluginModal", NoteInTab = "noteInTab", NoteInSplit = "noteInSplit", @@ -23,8 +24,9 @@ export enum UniversalDateOptions { } export enum TagColorType { - Text = "text", - Background = "background", + TagText = "text", + TagBg = "tagBg", + CardBg = "background", } export enum NotificationService { @@ -63,6 +65,9 @@ export enum taskPropertiesNames { Checkbox = "checkbox", SubTasksMinimized = "subTasksMinimized", DescriptionMinimized = "descriptionMinimized", + FilePathInHeader = "filePathInHeader", + ParentFolder = "parentFolder", + FullPath = "fullPath", } export enum DEFAULT_TASK_NOTE_FRONTMATTER_KEYS { @@ -89,6 +94,12 @@ export enum DEFAULT_TASK_NOTE_FRONTMATTER_KEYS { export enum viewTypeNames { kanban = "kanban", map = "map", + + // Upcoming views... + list = "list", + table = "table", + inbox = "inbox", + gantt = "gantt", } export enum defaultTaskStatuses { @@ -212,3 +223,16 @@ export enum scanModeOptions { AUTOMATIC = "automatic", MANUAL = "manual", } + +export enum HeaderUITypeOptions { + horizontal = "hor", + vertical = "vert", +} + +export enum viewsPanelPropertiesToShow { + Title = "title", + Description = "description", + progress = "progress", + CreatedDate = "createdDate", + ModifiedDate = "modifiedDate", +} diff --git a/src/interfaces/GlobalSettings.ts b/src/interfaces/GlobalSettings.ts index 037897c6..e09980f0 100644 --- a/src/interfaces/GlobalSettings.ts +++ b/src/interfaces/GlobalSettings.ts @@ -1,26 +1,9 @@ -import { TaskRegularExpressions } from "src/regularExpressions/TasksPluginRegularExpr"; -import { BoardConfigs } from "./BoardConfigs"; -import { - EditButtonMode, - TagColorType, - taskPropertiesNames, - taskPropertyFormatOptions, - UniversalDateOptions, - NotificationService, - DEFAULT_TASK_NOTE_FRONTMATTER_KEYS, - mapViewBackgrounVariantTypes, - mapViewNodeMapOrientation, - mapViewScrollAction, - mapViewArrowDirection, - mapViewEdgeType, - colTypeNames, - defaultTaskStatuses, - taskCardStyleNames, - scanModeOptions, -} from "./Enums"; -import { taskItemKeyToNameMapping } from "./Mapping"; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_TIME_FORMAT } from "./Constants.js"; +import { EditButtonMode, TagColorType, taskPropertiesNames, mapViewArrowDirection, mapViewScrollAction, mapViewEdgeType, taskPropertyFormatOptions, scanModeOptions, taskCardStyleNames, UniversalDateOptions, defaultTaskStatuses, DEFAULT_TASK_NOTE_FRONTMATTER_KEYS, NotificationService, mapViewBackgrounVariantTypes, mapViewNodeMapOrientation } from "./Enums.js"; +import { taskItemKeyToNameMapping } from "./Mapping.js"; -export interface scanFilters { + +export type ScanFilters = { files: { polarity: number; values: string[]; @@ -29,7 +12,7 @@ export interface scanFilters { polarity: number; values: string[]; }; - frontMatter: { + frontmatter: { polarity: number; values: string[]; }; @@ -60,23 +43,32 @@ export interface TaskBoardAction { targetColumn: string; } -export interface frontmatterFormatting { +export interface FrontmatterFormattingInterface { index: number; property: string; key: string; taskItemKey: string; } +export interface taskBoardFilesRegistryType { + [boardId: string]: { + boardId: string; + filePath: string; + boardName: string; + boardDescription: string; + }; +} + export interface globalSettingsData { openOnStartup: boolean; lang: string; - scanFilters: scanFilters; + scanFilters: ScanFilters; firstDayOfWeek?: string; ignoreFileNameDates: boolean; taskPropertyFormat: string; - taskCompletionDateTimePattern: string; dailyNotesPluginComp: boolean; - universalDateFormat: string; + dateFormat: string; + dateTimeFormat: string; defaultStartTime: string; taskCompletionInLocalTime: boolean; taskCompletionShowUtcOffset: boolean; @@ -84,13 +76,14 @@ export interface globalSettingsData { autoAddCreatedDate: boolean; autoAddCompletedDate: boolean; autoAddCancelledDate: boolean; - // scanVaultAtStartup: boolean; // @deprecated v1.9.0 - A better approach has been used using showModifiedFilesNotice feature. + // scanVaultAtStartup: boolean; - @deprecated v1.9.0 - A better approach has been used using showModifiedFilesNotice feature. showModifiedFilesNotice: boolean; scanMode: string; columnWidth: string; visiblePropertiesList: string[]; taskCardStyle: string; showVerticalScroll: boolean; + dragAutoScrollEdgePercent: number; tagColors: TagColor[]; editButtonAction: EditButtonMode; doubleClickCardToEdit: EditButtonMode; @@ -111,7 +104,7 @@ export interface globalSettingsData { archivedTasksFilePath: string; taskNoteDefaultLocation: string; archivedTBNotesFolderPath: string; - frontmatterFormatting: frontmatterFormatting[]; + frontmatterFormatting: FrontmatterFormattingInterface[]; showFrontmatterTagsOnCards: boolean; tasksCacheFilePath: string; notificationService: string; @@ -122,19 +115,13 @@ export interface globalSettingsData { uniqueIdCounter: number; // Counter to generate unique IDs for tasks. This will keep track of the last used ID. experimentalFeatures: boolean; safeGuardFeature: boolean; + taskBoardFilesRegistry: taskBoardFilesRegistryType; lastViewHistory: { - viewedType: string; - boardIndex: number; + boardFilePath: string; settingTab: number; taskId?: string; }; boundTaskCompletionToChildTasks: boolean; - kanbanView: { - lazyLoadingEnabled: boolean; - initialTaskCount: number; - loadMoreCount: number; - scrollThresholdPercent: number; - }; mapView: { background: string; mapOrientation: string; @@ -151,546 +138,312 @@ export interface globalSettingsData { // Define the interface for GlobalSettings based on your JSON structure export interface PluginDataJson { version: string; - data: { - boardConfigs: BoardConfigs; - globalSettings: globalSettingsData; - }; + data: globalSettingsData; } +/** + * @note There are hardcoded ids present in this data + * If you are changing the below configs, make sure the ids of + * two different objects are different. + */ export const DEFAULT_SETTINGS: PluginDataJson = { version: "", // Keep this empty only. Change the version number in the runOnPluginUpdate function inside main.ts file whenever you will going to release a new version. data: { - boardConfigs: [ + lang: "en", + openOnStartup: false, + scanFilters: { + files: { + polarity: 3, + values: [], + }, + folders: { + polarity: 3, + values: [], + }, + frontmatter: { + polarity: 3, + values: [], + }, + tags: { + polarity: 3, + values: [], + }, + }, + firstDayOfWeek: "Mon", + showTaskWithoutMetadata: true, + ignoreFileNameDates: false, + taskPropertyFormat: taskPropertyFormatOptions.tasksPlugin, + dailyNotesPluginComp: false, + dateFormat: DEFAULT_DATE_FORMAT, + dateTimeFormat: DEFAULT_DATE_TIME_FORMAT, + defaultStartTime: "", + taskCompletionInLocalTime: true, + taskCompletionShowUtcOffset: false, + autoAddUniversalDate: true, + autoAddCreatedDate: false, + autoAddCompletedDate: false, + autoAddCancelledDate: false, + showModifiedFilesNotice: true, + scanMode: scanModeOptions.AUTOMATIC, + columnWidth: "300px", + visiblePropertiesList: [ + taskPropertiesNames.Checkbox, + taskPropertiesNames.ID, + taskPropertiesNames.Title, + taskPropertiesNames.SubTasksMinimized, + taskPropertiesNames.DescriptionMinimized, + taskPropertiesNames.Status, + taskPropertiesNames.Tags, + taskPropertiesNames.Time, + taskPropertiesNames.Reminder, + taskPropertiesNames.Priority, + taskPropertiesNames.CreatedDate, + taskPropertiesNames.StartDate, + taskPropertiesNames.ScheduledDate, + taskPropertiesNames.DueDate, + taskPropertiesNames.CompletionDate, + taskPropertiesNames.CancelledDate, + taskPropertiesNames.Dependencies, + taskPropertiesNames.FilePath, + ], + taskCardStyle: taskCardStyleNames.EMOJI, + showVerticalScroll: true, + dragAutoScrollEdgePercent: 20, + tagColors: [ + { + name: "bug", + color: "rgba(255, 0, 0, 0.55)", + priority: 1, + }, + { + name: "important", + color: "rgba(246, 255, 0, 0.53)", + priority: 2, + }, + { + name: "wip", + color: "rgba(0, 255, 0, 0.53)", + priority: 3, + }, + { + name: "review", + color: "rgba(0, 0, 255, 0.49)", + priority: 4, + }, + ], + editButtonAction: EditButtonMode.Modal, + doubleClickCardToEdit: EditButtonMode.None, + universalDate: UniversalDateOptions.dueDate, + tagColorsType: TagColorType.TagText, + customStatuses: [ + { + symbol: defaultTaskStatuses.todo, + name: "Todo", + nextStatusSymbol: defaultTaskStatuses.done, + availableAsCommand: false, + type: "TODO", + }, + { + symbol: defaultTaskStatuses.scheduled, + name: "Ready to start", + nextStatusSymbol: defaultTaskStatuses.done, + availableAsCommand: false, + type: "TODO", + }, + { + symbol: defaultTaskStatuses.question, + name: "In Review", + nextStatusSymbol: defaultTaskStatuses.done, + availableAsCommand: false, + type: "TODO", + }, + { + symbol: defaultTaskStatuses.inprogress, + name: "In Progress", + nextStatusSymbol: defaultTaskStatuses.done, + availableAsCommand: true, + type: "IN_PROGRESS", + }, + { + symbol: defaultTaskStatuses.done, + name: "Done", + nextStatusSymbol: defaultTaskStatuses.todo, + availableAsCommand: true, + type: "DONE", + }, + { + symbol: defaultTaskStatuses.checked, + name: "Completed", + nextStatusSymbol: defaultTaskStatuses.todo, + availableAsCommand: true, + type: "DONE", + }, + { + symbol: defaultTaskStatuses.dropped, + name: "Cancelled", + nextStatusSymbol: defaultTaskStatuses.done, + availableAsCommand: true, + type: "CANCELLED", + }, + ], + compatiblePlugins: { + dailyNotesPlugin: false, + dayPlannerPlugin: false, + tasksPlugin: false, + reminderPlugin: false, + quickAddPlugin: false, + }, + taskNoteIdentifierTag: "taskNote", + preDefinedNote: "Meta/Task_Board/New_Tasks.md", + archivedTasksFilePath: "", + taskNoteDefaultLocation: "Meta/Task_Board/Task_Notes", + archivedTBNotesFolderPath: "Meta/Task_Board/Archived_Task_Notes", + quickAddPluginDefaultChoice: "", + frontmatterFormatting: [ { - columns: [ - { - id: 1, - colType: colTypeNames.undated, - active: true, - collapsed: false, - name: "Undated Tasks", - index: 1, - datedBasedColumn: { - dateType: "due", - from: 0, - to: 0, - }, - }, - { - id: 2, - colType: colTypeNames.dated, - active: true, - collapsed: false, - name: "Over Due", - index: 2, - datedBasedColumn: { - dateType: "due", - from: -300, - to: -1, - }, - }, - { - id: 3, - colType: colTypeNames.dated, - active: true, - collapsed: false, - name: "Today", - index: 3, - datedBasedColumn: { - dateType: "due", - from: 0, - to: 0, - }, - }, - { - id: 4, - colType: colTypeNames.dated, - active: true, - collapsed: false, - name: "Tomorrow", - index: 4, - datedBasedColumn: { - dateType: "due", - from: 1, - to: 1, - }, - }, - { - id: 5, - colType: colTypeNames.dated, - active: true, - collapsed: false, - name: "Future", - index: 5, - datedBasedColumn: { - dateType: "due", - from: 2, - to: 300, - }, - }, - { - id: 6, - colType: colTypeNames.completed, - active: true, - collapsed: false, - limit: 20, - name: "Completed", - index: 6, - }, - ], - name: "Time Based Workflow", index: 0, - showColumnTags: false, - showFilteredTags: true, - hideEmptyColumns: false, - boardFilter: { - rootCondition: "any", - filterGroups: [], - }, - swimlanes: { - enabled: false, - hideEmptySwimlanes: false, - property: "tags", - sortCriteria: "asc", - minimized: [], - maxHeight: "300px", - verticalHeaderUI: false, - }, + property: taskItemKeyToNameMapping["id"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.id, + taskItemKey: "id", }, { - columns: [ - { - id: 7, - colType: colTypeNames.untagged, - active: true, - collapsed: false, - name: "Backlogs", - index: 1, - }, - { - id: 8, - colType: colTypeNames.namedTag, - active: true, - collapsed: false, - name: "Important", - index: 2, - coltag: "important", - }, - { - id: 9, - colType: colTypeNames.namedTag, - active: true, - collapsed: false, - name: "WIP", - index: 3, - coltag: "wip", - }, - { - id: 11, - colType: colTypeNames.namedTag, - active: true, - collapsed: false, - name: "In Review", - index: 5, - coltag: "review", - }, - { - id: 12, - colType: colTypeNames.completed, - active: true, - collapsed: false, - index: 6, - limit: 20, - name: "Completed", - }, - ], - name: "Tag Based Workflow", index: 1, - showColumnTags: false, - showFilteredTags: true, - hideEmptyColumns: false, - boardFilter: { - rootCondition: "any", - filterGroups: [], - }, - swimlanes: { - enabled: false, - hideEmptySwimlanes: false, - property: "tags", - sortCriteria: "asc", - minimized: [], - maxHeight: "300px", - verticalHeaderUI: false, - }, + property: taskItemKeyToNameMapping["title"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.title, + taskItemKey: "title", }, { - columns: [ - { - id: 7, - colType: colTypeNames.taskStatus, - taskStatus: defaultTaskStatuses.unchecked, - active: true, - collapsed: false, - name: "Backlogs", - index: 1, - }, - { - id: 8, - colType: colTypeNames.taskStatus, - taskStatus: defaultTaskStatuses.scheduled, - active: true, - collapsed: false, - name: "Ready to start", - index: 2, - }, - { - id: 9, - colType: colTypeNames.taskStatus, - taskStatus: defaultTaskStatuses.inprogress, - active: true, - collapsed: false, - name: "In Progress", - index: 3, - }, - { - id: 11, - colType: colTypeNames.taskStatus, - taskStatus: defaultTaskStatuses.question, - active: true, - collapsed: false, - name: "In Review", - index: 5, - }, - { - id: 12, - colType: colTypeNames.completed, - active: true, - collapsed: false, - index: 6, - limit: 20, - name: "Completed", - }, - { - id: 13, - colType: colTypeNames.taskStatus, - taskStatus: defaultTaskStatuses.dropped, - active: true, - collapsed: false, - name: "Cancelled", - index: 7, - }, - ], - name: "Status Based Workflow", - index: 1, - showColumnTags: false, - showFilteredTags: true, - hideEmptyColumns: false, - boardFilter: { - rootCondition: "any", - filterGroups: [], - }, - swimlanes: { - enabled: false, - hideEmptySwimlanes: false, - property: "tags", - sortCriteria: "asc", - minimized: [], - maxHeight: "300px", - verticalHeaderUI: false, - }, + index: 2, + property: taskItemKeyToNameMapping["status"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.status, + taskItemKey: "status", }, - ], - globalSettings: { - lang: "en", - openOnStartup: false, - scanFilters: { - files: { - polarity: 3, - values: [], - }, - folders: { - polarity: 3, - values: [], - }, - frontMatter: { - polarity: 3, - values: [], - }, - tags: { - polarity: 3, - values: [], - }, + { + index: 3, + property: taskItemKeyToNameMapping["priority"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.priority, + taskItemKey: "priority", + }, + { + index: 4, + property: taskItemKeyToNameMapping["tags"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.tags, + taskItemKey: "tags", + }, + { + index: 5, + property: taskItemKeyToNameMapping["time"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.time, + taskItemKey: "time", + }, + { + index: 6, + property: taskItemKeyToNameMapping["reminder"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.reminder, + taskItemKey: "reminder", + }, + { + index: 7, + property: taskItemKeyToNameMapping["createdDate"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.createdDate, + taskItemKey: "createdDate", + }, + { + index: 8, + property: taskItemKeyToNameMapping["startDate"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.startDate, + taskItemKey: "startDate", + }, + { + index: 9, + property: taskItemKeyToNameMapping["scheduledDate"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.scheduledDate, + taskItemKey: "scheduledDate", + }, + { + index: 10, + property: taskItemKeyToNameMapping["due"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.due, + taskItemKey: "due", + }, + { + index: 11, + property: taskItemKeyToNameMapping["dependsOn"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.dependsOn, + taskItemKey: "dependsOn", }, - firstDayOfWeek: "Mon", - showTaskWithoutMetadata: true, - ignoreFileNameDates: false, - taskPropertyFormat: taskPropertyFormatOptions.tasksPlugin, - taskCompletionDateTimePattern: - TaskRegularExpressions.dateTimeFormat, - dailyNotesPluginComp: false, - universalDateFormat: TaskRegularExpressions.dateFormat, - defaultStartTime: "", - taskCompletionInLocalTime: true, - taskCompletionShowUtcOffset: false, - autoAddUniversalDate: true, - autoAddCreatedDate: false, - autoAddCompletedDate: false, - autoAddCancelledDate: false, - showModifiedFilesNotice: true, - scanMode: scanModeOptions.AUTOMATIC, - columnWidth: "300px", - visiblePropertiesList: [ - taskPropertiesNames.ID, - taskPropertiesNames.Title, - taskPropertiesNames.SubTasks, - taskPropertiesNames.Description, - taskPropertiesNames.Status, - taskPropertiesNames.Tags, - taskPropertiesNames.Priority, - taskPropertiesNames.CreatedDate, - taskPropertiesNames.StartDate, - taskPropertiesNames.ScheduledDate, - taskPropertiesNames.DueDate, - taskPropertiesNames.CompletionDate, - taskPropertiesNames.CancelledDate, - taskPropertiesNames.Reminder, - taskPropertiesNames.FilePath, - ], - taskCardStyle: taskCardStyleNames.EMOJI, - showVerticalScroll: true, - tagColors: [ - { - name: "bug", - color: "rgba(255, 0, 0, 0.55)", - priority: 1, - }, - { - name: "important", - color: "rgba(246, 255, 0, 0.53)", - priority: 2, - }, - { - name: "wip", - color: "rgba(0, 255, 0, 0.53)", - priority: 2, - }, - { - name: "review", - color: "rgba(0, 0, 255, 0.49)", - priority: 3, - }, - ], - editButtonAction: EditButtonMode.Modal, - doubleClickCardToEdit: EditButtonMode.None, - universalDate: UniversalDateOptions.dueDate, - tagColorsType: TagColorType.Background, - customStatuses: [ - { - symbol: defaultTaskStatuses.todo, - name: "Todo", - nextStatusSymbol: defaultTaskStatuses.done, - availableAsCommand: false, - type: "TODO", - }, - { - symbol: defaultTaskStatuses.scheduled, - name: "Ready to start", - nextStatusSymbol: defaultTaskStatuses.done, - availableAsCommand: false, - type: "TODO", - }, - { - symbol: defaultTaskStatuses.question, - name: "In Review", - nextStatusSymbol: defaultTaskStatuses.done, - availableAsCommand: false, - type: "TODO", - }, - { - symbol: defaultTaskStatuses.inprogress, - name: "In Progress", - nextStatusSymbol: defaultTaskStatuses.done, - availableAsCommand: true, - type: "IN_PROGRESS", - }, - { - symbol: defaultTaskStatuses.done, - name: "Done", - nextStatusSymbol: defaultTaskStatuses.todo, - availableAsCommand: true, - type: "DONE", - }, - { - symbol: defaultTaskStatuses.checked, - name: "Completed", - nextStatusSymbol: defaultTaskStatuses.todo, - availableAsCommand: true, - type: "DONE", - }, - { - symbol: defaultTaskStatuses.dropped, - name: "Cancelled", - nextStatusSymbol: defaultTaskStatuses.done, - availableAsCommand: true, - type: "CANCELLED", - }, - ], - compatiblePlugins: { - dailyNotesPlugin: false, - dayPlannerPlugin: false, - tasksPlugin: false, - reminderPlugin: false, - quickAddPlugin: false, + { + index: 12, + property: taskItemKeyToNameMapping["completion"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.cancelledDate, + taskItemKey: "cancelledDate", }, - preDefinedNote: "Meta/Task_Board/New_Tasks.md", - taskNoteIdentifierTag: "taskNote", - taskNoteDefaultLocation: "Meta/Task_Board/Task_Notes", - quickAddPluginDefaultChoice: "", - archivedTasksFilePath: "", - archivedTBNotesFolderPath: "Meta/Task_Board/Archived_Task_Notes", - frontmatterFormatting: [ - { - index: 0, - property: taskItemKeyToNameMapping["id"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.id, - taskItemKey: "id", - }, - { - index: 1, - property: taskItemKeyToNameMapping["title"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.title, - taskItemKey: "title", - }, - { - index: 2, - property: taskItemKeyToNameMapping["status"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.status, - taskItemKey: "status", - }, - { - index: 3, - property: taskItemKeyToNameMapping["priority"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.priority, - taskItemKey: "priority", - }, - { - index: 4, - property: taskItemKeyToNameMapping["tags"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.tags, - taskItemKey: "tags", - }, - { - index: 5, - property: taskItemKeyToNameMapping["time"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.time, - taskItemKey: "time", - }, - { - index: 6, - property: taskItemKeyToNameMapping["reminder"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.reminder, - taskItemKey: "reminder", - }, - { - index: 7, - property: taskItemKeyToNameMapping["createdDate"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.createdDate, - taskItemKey: "createdDate", - }, - { - index: 8, - property: taskItemKeyToNameMapping["startDate"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.startDate, - taskItemKey: "startDate", - }, - { - index: 9, - property: taskItemKeyToNameMapping["scheduledDate"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.scheduledDate, - taskItemKey: "scheduledDate", - }, - { - index: 10, - property: taskItemKeyToNameMapping["due"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.due, - taskItemKey: "due", - }, - { - index: 11, - property: taskItemKeyToNameMapping["dependsOn"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.dependsOn, - taskItemKey: "dependsOn", - }, - { - index: 12, - property: taskItemKeyToNameMapping["completion"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.cancelledDate, - taskItemKey: "cancelledDate", - }, - { - index: 13, - property: taskItemKeyToNameMapping["completionDate"], - key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.completionDate, - taskItemKey: "completionDate", - }, - // TODO : The below properties will be available once the TBNote feature has been implemented. The filePath will be actually the path of the task-note or the tb-note. A new property will be required to be added inside the taskItem interface to store the sourcePath. - // { - // index: 14, - // property: taskItemKeyToNameMapping["filePath"], - // key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.filePath, - // taskItemKey: "filePath", - // }, - // { - // index: 15, - // property: taskItemKeyToNameMapping["taskLocation"], - // key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.taskLocation, - // taskItemKey: "taskLocation", - // }, - // { - // index: 14, - // property: taskItemKeyToNameMapping["dateModified"], - // key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.dateModified, - // taskItemKey: "", - // }, - ], - showFrontmatterTagsOnCards: false, - tasksCacheFilePath: "", - notificationService: NotificationService.None, - actions: [ - { - enabled: true, - trigger: "Complete", - type: "move", - targetColumn: "Completed", - }, - ], - hiddenTaskProperties: [], - autoAddUniqueID: false, - uniqueIdCounter: 0, // Counter to generate unique IDs for tasks. This will keep track of the last used ID. --- IGNORE --- - experimentalFeatures: false, - safeGuardFeature: true, - lastViewHistory: { - viewedType: "kanban", - boardIndex: 0, - settingTab: 0, + { + index: 13, + property: taskItemKeyToNameMapping["completionDate"], + key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.completionDate, + taskItemKey: "completionDate", }, - boundTaskCompletionToChildTasks: false, - kanbanView: { - lazyLoadingEnabled: true, - initialTaskCount: 20, - loadMoreCount: 10, - scrollThresholdPercent: 80, + // TODO : The below properties will be available once the TBNote feature has been implemented. The filePath will be actually the path of the task-note or the tb-note. A new property will be required to be added inside the taskItem interface to store the sourcePath. + // { + // index: 14, + // property: taskItemKeyToNameMapping["filePath"], + // key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.filePath, + // taskItemKey: "filePath", + // }, + // { + // index: 15, + // property: taskItemKeyToNameMapping["taskLocation"], + // key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.taskLocation, + // taskItemKey: "taskLocation", + // }, + // { + // index: 14, + // property: taskItemKeyToNameMapping["dateModified"], + // key: DEFAULT_TASK_NOTE_FRONTMATTER_KEYS.dateModified, + // taskItemKey: "", + // }, + ], + showFrontmatterTagsOnCards: false, + tasksCacheFilePath: "", + notificationService: NotificationService.None, + actions: [ + { + enabled: true, + trigger: "Complete", + type: "move", + targetColumn: "Completed", }, - mapView: { - background: mapViewBackgrounVariantTypes.none, - mapOrientation: mapViewNodeMapOrientation.horizontal, - optimizedRender: false, - arrowDirection: mapViewArrowDirection.childToParent, - animatedEdges: true, - scrollAction: mapViewScrollAction.zoom, - showMinimap: true, - renderVisibleNodes: false, - edgeType: mapViewEdgeType.bezier, + ], + hiddenTaskProperties: [], + autoAddUniqueID: false, + uniqueIdCounter: 0, // Counter to generate unique IDs for tasks. This will keep track of the last used ID. --- IGNORE --- + experimentalFeatures: false, + safeGuardFeature: true, + lastViewHistory: { + boardFilePath: + "Meta/Task_Board/Boards/Time Based Workflow.taskboard", + settingTab: 0, + }, + boundTaskCompletionToChildTasks: false, + mapView: { + background: mapViewBackgrounVariantTypes.none, + mapOrientation: mapViewNodeMapOrientation.horizontal, + optimizedRender: false, + arrowDirection: mapViewArrowDirection.childToParent, + animatedEdges: true, + scrollAction: mapViewScrollAction.zoom, + showMinimap: true, + renderVisibleNodes: false, + edgeType: mapViewEdgeType.bezier, + }, + taskBoardFilesRegistry: { + "3103563481": { + boardId: "3103563481", + filePath: "Meta/Task_Board/Boards/My Project Board.taskboard", + boardName: "Time Based Workflow", + boardDescription: + "This board contains dated type columns for managing time critical scheduled tasks.", }, }, }, diff --git a/src/interfaces/Mapping.ts b/src/interfaces/Mapping.ts index cb90083a..f2e7ef37 100644 --- a/src/interfaces/Mapping.ts +++ b/src/interfaces/Mapping.ts @@ -1,7 +1,8 @@ -import { t } from "src/utils/lang/helper"; -import { defaultTaskStatuses } from "./Enums"; -import { taskItem } from "./TaskItem"; -import { CustomStatus } from "./GlobalSettings"; +import { bugReporterManagerInsatance } from "../managers/BugReporter.js"; +import { t } from "../utils/lang/helper.js"; +import { defaultTaskStatuses } from "./Enums.js"; +import { CustomStatus } from "./GlobalSettings.js"; +import { taskItem } from "./TaskItem.js"; export const priorityEmojis: { [key: number]: string } = { 0: "0", @@ -14,6 +15,7 @@ export const priorityEmojis: { [key: number]: string } = { export interface statusDropDownOption { value: string; + name: string; text: string; } @@ -40,16 +42,190 @@ export const getPriorityOptionsForDropdown = (): priorityDropDownOption[] => [ // Legacy export for backward compatibility export const priorityOptions = getPriorityOptionsForDropdown(); +export interface StatusDropdownOption { + value: string; // The symbol used as option value + label: string; // Display text: "Name [symbol]" + tooltip?: string; // Optional hover text + group?: string; // Optional group/type for optgroup + metadata?: CustomStatus; // Full status object for advanced use +} + +export interface GroupedStatusOptions { + type: string; + label: string; // Human-readable group label + options: StatusDropdownOption[]; +} + +export type StatusDropdownOutput = + | { type: "flat"; options: StatusDropdownOption[] } + | { type: "grouped"; groups: GroupedStatusOptions[] }; + +export interface GetCustomStatusOptionsConfig { + mode?: "flat" | "grouped"; // Output format + includePlaceholder?: boolean; // Add "Select..." option + placeholderText?: string; // Custom placeholder text + showTooltips?: boolean; // Include tooltip with next status + formatLabel?: (status: CustomStatus) => string; // Custom label formatter + groupLabelFormatter?: (type: string) => string; // Custom group label + validateSymbols?: boolean; // Check for duplicate symbols +} + +/** + * Generates dropdown options from CustomStatus array with validation and grouping support. + * + * @param statusConfigs - Array of CustomStatus objects from settings + * @param config - Optional configuration for output format and behavior + * @returns Structured options ready for rendering in handleBoardNameChange(boardIndex, e.target.value)} - /> -
-
-
-
{t("board-description")}
-
{t("board-description-info")}
-
-