From 9e1e7cd459bb98a2d41a1ef7648c040cd1381339 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sat, 26 Jul 2025 18:58:20 -0700 Subject: [PATCH 1/2] Fix getFrontmatterAttribute which was causing Rollup to fail --- jgclark.Dashboard/src/dataGenerationTags.js | 43 +++++++++++---------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/jgclark.Dashboard/src/dataGenerationTags.js b/jgclark.Dashboard/src/dataGenerationTags.js index 7c890beda..2df350b54 100644 --- a/jgclark.Dashboard/src/dataGenerationTags.js +++ b/jgclark.Dashboard/src/dataGenerationTags.js @@ -7,18 +7,20 @@ import moment from 'moment/min/moment-with-locales' import type { TDashboardSettings, TSection, TSectionItem, TSectionDetails } from './types' import { getNumCompletedTasksFromNote } from './countDoneTasks' -import { - createSectionItemObject, - isLineDisallowedByIgnoreTerms, - makeDashboardParas, -} from './dashboardHelpers' +import { createSectionItemObject, isLineDisallowedByIgnoreTerms, makeDashboardParas } from './dashboardHelpers' import { tagParasFromNote } from './demoData' -import { addTagMentionCacheDefinitions, getFilenamesOfNotesWithTagOrMentions, isTagMentionCacheAvailableforItem, scheduleTagMentionCacheGeneration, WANTED_PARA_TYPES } from './tagMentionCache' +import { + addTagMentionCacheDefinitions, + getFilenamesOfNotesWithTagOrMentions, + isTagMentionCacheAvailableforItem, + scheduleTagMentionCacheGeneration, + WANTED_PARA_TYPES, +} from './tagMentionCache' import { filenameIsInFuture, includesScheduledFutureDate } from '@helpers/dateTime' import { stringListOrArrayToArray } from '@helpers/dataManipulation' import { clo, logDebug, logError, logInfo, logTimer, timer } from '@helpers/dev' import { getFolderFromFilename } from '@helpers/folders' -import { getFrontMatterAttribute, noteHasFrontMatter } from '@helpers/NPFrontMatter' +import { getFrontmatterAttribute, noteHasFrontMatter } from '@helpers/NPFrontMatter' import { getNoteByFilename } from '@helpers/note' import { findNotesMatchingHashtagOrMention, getHeadingsFromNote } from '@helpers/NPnote' import { sortListBy } from '@helpers/sorting' @@ -80,10 +82,13 @@ export async function getTaggedSectionData(config: TDashboardSettings, useDemoDa const cacheIsAvailable = isTagMentionCacheAvailableforItem(thisTag) if (config.FFlag_UseTagCache && cacheIsAvailable) { // Use Cache - logInfo('getTaggedSectionData', `- using cache for - ${thisTag}`) + logInfo( + 'getTaggedSectionData', + `- using cache for + ${thisTag}`, + ) let filenamesWithTagFromCache: Array = [] - ;[filenamesWithTagFromCache, comparisonDetails] = await getFilenamesOfNotesWithTagOrMentions([thisTag], true, turnOnAPIComparison) + ;[filenamesWithTagFromCache, comparisonDetails] = await getFilenamesOfNotesWithTagOrMentions([thisTag], true, turnOnAPIComparison) // This is taking about 2ms per note for JGC filenamesWithTagFromCache.forEach((filename) => { @@ -97,7 +102,7 @@ export async function getTaggedSectionData(config: TDashboardSettings, useDemoDa logTimer('getTaggedSectionData', thisStartTime, `- from CACHE found ${notesWithTag.length} notes with ${thisTag}`) // $FlowIgnore[unsafe-arithmetic] // cacheLookupTime = new Date() - cachedOperationStartTime - source = (turnOnAPIComparison) ? 'using CACHE + API' : 'using just CACHE' + source = turnOnAPIComparison ? 'using CACHE + API' : 'using just CACHE' } else { // Use API logInfo('getTaggedSectionData', `- using API only for ${thisTag}`) @@ -126,18 +131,16 @@ export async function getTaggedSectionData(config: TDashboardSettings, useDemoDa // If we want to use note tags, and the note has a 'note-tag' field in its FM, then work out if the note-tag matches this particular tag/mention. let hasMatchingNoteTag = false if (noteHasFrontMatter(n)) { - const noteTagAttribute = getFrontMatterAttribute(n, 'note-tag') + const noteTagAttribute = getFrontmatterAttribute(n, 'note-tag') const noteTagList = noteTagAttribute ? stringListOrArrayToArray(noteTagAttribute, ',') : [] if (noteTagList.length > 0) { - hasMatchingNoteTag = noteTagList && noteTagList.some(tag => caseInsensitiveMatch(tag, thisTag)) + hasMatchingNoteTag = noteTagList && noteTagList.some((tag) => caseInsensitiveMatch(tag, thisTag)) logInfo('getTaggedSectionData', `-> noteTag(s) '${String(noteTagList)}' is ${hasMatchingNoteTag ? 'a' : 'NOT a'} match for ${thisTag}`) } } // Add the paras that contain the tag/mention, unless this is a noteTag, in which case add all paras if FM field 'note-tag' matches. (Later we filter down to open non-scheduled items). - const tagParasFromNote = (hasMatchingNoteTag) - ? paras - : paras.filter((p) => caseInsensitiveSubstringMatch(thisTag, p.content)) + const tagParasFromNote = hasMatchingNoteTag ? paras : paras.filter((p) => caseInsensitiveSubstringMatch(thisTag, p.content)) logTimer('getTaggedSectionData', thisStartTime, `- found ${tagParasFromNote.length} ${thisTag} items in "${n.filename}"`) // Further filter out checklists and otherwise empty items @@ -203,10 +206,10 @@ export async function getTaggedSectionData(config: TDashboardSettings, useDemoDa config.overdueSortOrder === 'priority' ? ['-priority', '-changedDate'] : config.overdueSortOrder === 'earliest' - ? ['changedDate', '-priority'] - : config.overdueSortOrder === 'due date' - ? ['dueDate', '-priority'] - : ['-changedDate', '-priority'] // 'most recent' + ? ['changedDate', '-priority'] + : config.overdueSortOrder === 'due date' + ? ['dueDate', '-priority'] + : ['-changedDate', '-priority'] // 'most recent' const sortedTagParas = sortListBy(dashboardParas, sortOrder) logTimer('getTaggedSectionData', thisStartTime, `- Filtered, Reduced & Sorted ${sortedTagParas.length} items by ${String(sortOrder)}`) From 77c9a43eedeb8491c7c42ff7256e073d31367184 Mon Sep 17 00:00:00 2001 From: David Wertheimer Date: Sat, 26 Jul 2025 19:09:54 -0700 Subject: [PATCH 2/2] Remove processByTimeBlockTag circularity --- .../__tests__/timeblocking-helpers.test.js | 2 +- dwertheimer.EventAutomations/src/byTagMode.js | 225 +---------------- .../src/timeblocking-helpers.js | 229 +++++++++++++++++- 3 files changed, 230 insertions(+), 226 deletions(-) diff --git a/dwertheimer.EventAutomations/__tests__/timeblocking-helpers.test.js b/dwertheimer.EventAutomations/__tests__/timeblocking-helpers.test.js index e7d2b8e0a..16fd70a9b 100644 --- a/dwertheimer.EventAutomations/__tests__/timeblocking-helpers.test.js +++ b/dwertheimer.EventAutomations/__tests__/timeblocking-helpers.test.js @@ -2,7 +2,7 @@ // import colors from 'chalk' // import /* differenceInCalendarDays, endOfDay, startOfDay, eachMinuteOfInterval, */ 'date-fns' import * as tb from '../src/timeblocking-helpers' -import * as byTagMode from '../src/byTagMode' +import * as byTagMode from '../src/timeblocking-helpers' import { getTasksByType } from '@helpers/sorting' import { JSP } from '@helpers/dev' diff --git a/dwertheimer.EventAutomations/src/byTagMode.js b/dwertheimer.EventAutomations/src/byTagMode.js index 82215aa00..08ed40416 100644 --- a/dwertheimer.EventAutomations/src/byTagMode.js +++ b/dwertheimer.EventAutomations/src/byTagMode.js @@ -7,227 +7,6 @@ import type { OpenBlock, ParagraphWithDuration, TimeBlocksWithMap } from './time import { filterTimeMapToOpenSlots, findTimeBlocks, matchTasksToSlots, namedTagExistsInLine, splitItemsByTags } from './timeblocking-helpers' import { JSP, clo, log, logError, logWarn, logDebug, clof, deepCopy } from '@helpers/dev' -/** - * Processes the tasks that have a named tag in them (e.g., @work or #work) - * and schedules them within the specified time blocks. - * - * @param {Array} sortedTaskList - The list of tasks sorted by some criteria. - * @param {TimeBlockWithMap} tmb - The current time block with map. - * @param {Config} config - Configuration options for processing. - * @returns {TimeBlockWithMap} - The updated time block with map after processing. - */ -export function processByTimeBlockTag(sortedTaskList: Array, tmb: TimeBlocksWithMap, config: AutoTimeBlockingConfig): TimeBlocksWithMap { - // Destructure the initial time block with map structure - const { blockList, timeMap } = tmb - // Process tasks by matching them to named time blocks based on tags - const { newBlockList, unprocessedTasks, results, noTimeForTasks } = processTasksByTimeBlockTag(sortedTaskList, blockList || [], timeMap, config) - - // Handle the unprocessed tasks according to the specified orphanTaggedTasks strategy - const unprocessedTasksResult = handleUnprocessedTasks(unprocessedTasks, noTimeForTasks, config, newBlockList, timeMap) - - // Combine results from named blocks processing and final processing - const combinedResults = [...results, unprocessedTasksResult] - const combinedNoTimeForTasks = { ...noTimeForTasks, ...unprocessedTasksResult.noTimeForTasks } - - // Prepare the final return structure - return { - noTimeForTasks: combinedNoTimeForTasks, - timeMap, - blockList: newBlockList, - timeBlockTextList: combinedResults.reduce((acc, currentValue) => acc.concat(currentValue.timeBlockTextList), []).sort(), - } -} - -/** - * Handles the rest of the non-named-block/unprocessed tasks according to the specified orphanTaggedTasks strategy in the config. - * - * @param {Array} unprocessedTasks - List of tasks that remain unprocessed. - * @param { [key: string]: Array } noTimeForTasks - Object containing tasks for which no time could be found, keyed by block title. - * @param {AutoTimeBlockingConfig} config - Configuration options for processing. - * @param {Array} newBlockList - The list of new blocks after processing. - * @param {Array} timeMap - The current time map. - * @returns {Object} - Returns the final result of matching tasks to slots, including unprocessed tasks handled as per config. - */ -export function handleUnprocessedTasks( - unprocessedTasks: Array, - noTimeForTasks: { [key: string]: Array }, - config: AutoTimeBlockingConfig, - newBlockList: Array, - timeMap: Array, -): TimeBlocksWithMap { - let finalUnprocessedTasks = unprocessedTasks || [] - const noTimeTasks = Object.values(noTimeForTasks)?.flat() || [] - clof(noTimeTasks, `handleUnprocessedTasks noTimeTasks=`, ['content', 'duration'], true) - - switch (config.orphanTagggedTasks) { - case 'IGNORE_THEM': - // If ignoring, do nothing further with noTimeTasks - break - case "OUTPUT_FOR_INFO (but don't schedule them)": - // If outputting for info, log or store these tasks separately (not shown here) - break - case 'SCHEDULE_ELSEWHERE_LAST': - finalUnprocessedTasks = [...finalUnprocessedTasks, ...noTimeTasks] - break - case 'SCHEDULE_ELSEWHERE_FIRST': - finalUnprocessedTasks = [...noTimeTasks, ...finalUnprocessedTasks] - break - } - - config.mode = 'PRIORITY_FIRST' - clof(finalUnprocessedTasks, `handleUnprocessedTasks finalUnprocessedTasks=`, 'content', true) - - return matchTasksToSlots(finalUnprocessedTasks, { blockList: newBlockList, timeMap }, config) -} - -/** - * Processes tasks by matching them to named time blocks based on tags. - * - * @param {Array} sortedTaskList - The list of tasks sorted by some criteria. - * @param {Array} blockList - The current list of time blocks. - * @param {Array} timeMap - The current time map. - * @param {Config} config - Configuration options for processing. - * @returns {Object} - Returns an object containing the updated block list, unprocessed tasks, results, and no time for tasks. - */ -export function processTasksByTimeBlockTag(sortedTaskList: Array, blockList: Array, timeMap: Array, config: AutoTimeBlockingConfig): Object { - let newBlockList = [...(blockList || [])] - let results = [] - let timeBlockTextList: any = [] - const noTimeForTasks = {} - - // MOVE THIS TO ITS OWN FUNCTION - // Split tasks into matched and unmatched based on tags - clo(config.timeframes, `processTasksByTimeBlockTag config.timeframes=`) - clo(blockList, `processTasksByTimeBlockTag blockList=`) - const { matched, unmatched } = splitItemsByTags(sortedTaskList, config.timeframes || {}) - let unprocessedTasks = unmatched || [] // tasks that do not match a timeframe will flow through to the next processing step - clof(matched, `processTasksByTimeBlockTag matched=`, null, true) - clof(unmatched, `processTasksByTimeBlockTag unmatched=`, ['content'], true) - const keys = Object.keys(matched) - if (keys.length) { - logDebug(`"STARTING TIMEFRAME PROCESSING": ${keys.length} timeframes matched in tasks`) - let newTimeMapWithBlocks = { timeBlockTextList: [], timeMap: [], blockList: [] } - // process tasks by timeframe key - keys.forEach((key) => { - const tasksMatchingThisTimeframe = matched[key] - const [start, end] = config.timeframes[key] - let timeMapCopy = deepCopy(timeMap) // timeMap.slice() - // process one task in the timeframe at a time - const sortedTasksMatchingTimeframe = sortListBy(tasksMatchingThisTimeframe, ['-priority', '-duration']) - sortedTasksMatchingTimeframe.forEach((task) => { - logDebug(`processTasksByTimeBlockTag TIMEFRAME:"${key}": start=${start} end=${end}`) - // blank out all slots that are not in the timeframe in question - timeMapCopy.forEach((t, i) => { - if (t.start < start || t.start >= end) { - timeMapCopy[i].busy = true // remove times from consideration that are not in the timeframe in question - } - }) - // filter the map to only open slots and then find open timeblocks - const openTimesForTimeframe = filterTimeMapToOpenSlots(timeMapCopy, config) - const blocksForTimeframe = findTimeBlocks(openTimesForTimeframe, config) - clof(blocksForTimeframe, `processTasksByTimeBlockTag blocksForTimeframe ${key} =`, ['start', 'minsAvailable'], true) - newTimeMapWithBlocks = matchTasksToSlots([task], { blockList: blocksForTimeframe, timeMap: openTimesForTimeframe }, config) - results.push(newTimeMapWithBlocks) - - const { timeMap: timeMapAfterTimeframePlacement, noTimeForTasks: nTftAfterTimeframePlacement, timeBlockTextList: timeBlockTextListAfterPlacement } = newTimeMapWithBlocks - timeMapCopy = timeMapAfterTimeframePlacement - // update the master timeMap with the changes that were made - // timemap slots that were used will be missing in the result, so we will just mark them as busy in the master timeMap - // function: updateMasterTimeMapWithTimeMapChanges - openTimesForTimeframe.forEach((t, i) => { - if (!timeMapAfterTimeframePlacement.find((nt) => nt.start === t.start)) { - ;(timeMap.find((tm) => tm.start === t.start) ?? {}).busy = true - } - }) - // save no time for tasks - if (nTftAfterTimeframePlacement) { - Object.keys(nTftAfterTimeframePlacement).forEach((key) => { - if (!noTimeForTasks[key]) noTimeForTasks[key] = [] - noTimeForTasks[key] = noTimeForTasks[key].concat(nTftAfterTimeframePlacement[key]) - }) - } - // save timeblocktextlist - if (timeBlockTextListAfterPlacement.length) { - timeBlockTextList = timeBlockTextList.concat(timeBlockTextListAfterPlacement) - } - //FIXME: I am here -- need to fix the circular dependency oy vey - }) - }) - } - clof(timeBlockTextList, `processTasksByTimeBlockTag timeBlockTextList=`, null, false) - clof(noTimeForTasks, `processTasksByTimeBlockTag noTimeForTasks=`, ['_', 'content'], true) - // end split - - logDebug(`"STARTING BY TIMEBLOCK TAG PROCESSING": ${unprocessedTasks.length} unprocessedTasks`) - clof(unprocessedTasks, `processTasksByTimeBlockTag unprocessedTasks=`, ['content'], true) - const filteredMap = filterTimeMapToOpenSlots(timeMap, config) - newBlockList = findTimeBlocks(filteredMap, config) - const namedBlocks = getNamedTimeBlocks(newBlockList ?? []) - namedBlocks.forEach((block) => { - const blockName = block.title || '' - logDebug(`PROCESSING BLOCK: "${blockName}" (tasks will be limited to this tag): ${unprocessedTasks.length} unprocessedTasks`) - const { - unprocessedTasks: updatedUnprocessedTasks, - results: blockResults, - noTimeForTasks: blockNoTimeForTasks, - } = processTasksForNamedTimeBlock(block, unprocessedTasks, timeMap, config) - clof(updatedUnprocessedTasks, `processTasksByTimeBlockTag updatedUnprocessedTasks after looking for block "${blockName}"`, null, true) - unprocessedTasks = updatedUnprocessedTasks - results = results.concat(blockResults) - Object.keys(blockNoTimeForTasks).forEach((key) => { - if (!noTimeForTasks[key]) noTimeForTasks[key] = [] - noTimeForTasks[key] = noTimeForTasks[key].concat(blockNoTimeForTasks[key]) - }) - - newBlockList = newBlockList.filter((b) => b !== block) - }) - - return { newBlockList, unprocessedTasks, results, noTimeForTasks } -} - -/** - * Get the timeblocks that have names/titles (e.g. a user set them up "Work" or "Home" or whatever) - * @param {Array} blockList - * @param {*} config - * @returns {Array} the filtered blockList - */ -export function getNamedTimeBlocks(blockList: Array): Array { - return blockList.filter((b) => b.title && b.title !== '') -} - -/** - * Processes tasks for a single named time block, updating tasks and no time for tasks accordingly. - * - * @param {TimeBlock} block - The current time block being processed. - * @param {Array} unprocessedTasks - List of tasks that have not been processed yet. - * @param {Array} timeMap - The current time map. - * @param {Config} config - Configuration options for processing. - * @returns {Object} - Returns an object containing the updated list of unprocessed tasks, results, and no time for tasks for this block. - */ -export function processTasksForNamedTimeBlock( - block: OpenBlock, - incomingUnprocessedTasks: Array, - timeMap: Array, - config: AutoTimeBlockingConfig, -): Object { - const results = [] - const noTimeForTasks = {} - const blockTitle = (block.title || '').replace(config.timeblockTextMustContainString, '').replace(/ {2,}/g, ' ').trim() - let unprocessedTasks = incomingUnprocessedTasks - const tasksMatchingThisNamedTimeblock = unprocessedTasks.filter((task) => block.title && namedTagExistsInLine(blockTitle, task.content)) - tasksMatchingThisNamedTimeblock.forEach((task, i) => { - const filteredTimeMap = timeMap.filter((t) => typeof t.busy === 'string' && t.busy.includes(blockTitle) && t.busy.includes(config.timeblockTextMustContainString)) - const newTimeBlockWithMap = matchTasksToSlots([task], { blockList: [block], timeMap: filteredTimeMap }, config) - unprocessedTasks = unprocessedTasks.filter((t) => t !== task) - const foundTimeForTask = newTimeBlockWithMap.timeBlockTextList && newTimeBlockWithMap.timeBlockTextList.length > 0 - if (foundTimeForTask) { - results.push(newTimeBlockWithMap) - } else { - if (!noTimeForTasks[blockTitle]) noTimeForTasks[blockTitle] = [] - noTimeForTasks[blockTitle].push(task) - } - }) - clof(unprocessedTasks, `processTasksForNamedTimeBlock newUnprocessedTasks after looking for block "${blockTitle}"`, null, true) - return { unprocessedTasks, results, noTimeForTasks } -} +// All functions have been moved to timeblocking-helpers.js to resolve circular dependency +// This file now only contains imports and can be removed if no longer needed diff --git a/dwertheimer.EventAutomations/src/timeblocking-helpers.js b/dwertheimer.EventAutomations/src/timeblocking-helpers.js index 25161ca86..534884b70 100644 --- a/dwertheimer.EventAutomations/src/timeblocking-helpers.js +++ b/dwertheimer.EventAutomations/src/timeblocking-helpers.js @@ -16,13 +16,13 @@ import type { Tags, } from './timeblocking-flow-types' import type { AutoTimeBlockingConfig } from './config' -import { processByTimeBlockTag } from './byTagMode' + import { getDateObjFromDateTimeString, getTimeStringFromDate, removeRepeats } from '@helpers/dateTime' import { sortListBy } from '@helpers/sorting' import { removeDateTagsAndToday } from '@helpers/stringTransforms' import { textWithoutSyncedCopyTag } from '@helpers/syncedCopies' import { createPrettyLinkToLine, createWikiLinkToLine } from '@helpers/NPSyncedCopies' -import { logError, JSP, copyObject, clo, clof, logDebug, logWarn } from '@helpers/dev' +import { logError, JSP, copyObject, clo, clof, logDebug, logWarn, deepCopy } from '@helpers/dev' // import { timeblockRegex1, timeblockRegex2 } from '../../helpers/markdown-regex' @@ -754,3 +754,228 @@ export function getFullParagraphsCorrespondingToSortList(paragraphs: Array} blockList + * @param {*} config + * @returns {Array} the filtered blockList + */ +export function getNamedTimeBlocks(blockList: Array): Array { + return blockList.filter((b) => b.title && b.title !== '') +} + +/** + * Processes tasks for a single named time block, updating tasks and no time for tasks accordingly. + * + * @param {TimeBlock} block - The current time block being processed. + * @param {Array} unprocessedTasks - List of tasks that have not been processed yet. + * @param {Array} timeMap - The current time map. + * @param {Config} config - Configuration options for processing. + * @returns {Object} - Returns an object containing the updated list of unprocessed tasks, results, and no time for tasks for this block. + */ +export function processTasksForNamedTimeBlock( + block: OpenBlock, + incomingUnprocessedTasks: Array, + timeMap: Array, + config: AutoTimeBlockingConfig, +): Object { + const results = [] + const noTimeForTasks = {} + const blockTitle = (block.title || '').replace(config.timeblockTextMustContainString, '').replace(/ {2,}/g, ' ').trim() + let unprocessedTasks = incomingUnprocessedTasks + const tasksMatchingThisNamedTimeblock = unprocessedTasks.filter((task) => block.title && namedTagExistsInLine(blockTitle, task.content)) + tasksMatchingThisNamedTimeblock.forEach((task, i) => { + const filteredTimeMap = timeMap.filter((t) => typeof t.busy === 'string' && t.busy.includes(blockTitle) && t.busy.includes(config.timeblockTextMustContainString)) + const newTimeBlockWithMap = matchTasksToSlots([task], { blockList: [block], timeMap: filteredTimeMap }, config) + unprocessedTasks = unprocessedTasks.filter((t) => t !== task) + const foundTimeForTask = newTimeBlockWithMap.timeBlockTextList && newTimeBlockWithMap.timeBlockTextList.length > 0 + if (foundTimeForTask) { + results.push(newTimeBlockWithMap) + } else { + if (!noTimeForTasks[blockTitle]) noTimeForTasks[blockTitle] = [] + noTimeForTasks[blockTitle].push(task) + } + }) + clof(unprocessedTasks, `processTasksForNamedTimeBlock newUnprocessedTasks after looking for block "${blockTitle}"`, null, true) + return { unprocessedTasks, results, noTimeForTasks } +} + +/** + * Handles the rest of the non-named-block/unprocessed tasks according to the specified orphanTaggedTasks strategy in the config. + * + * @param {Array} unprocessedTasks - List of tasks that remain unprocessed. + * @param { [key: string]: Array } noTimeForTasks - Object containing tasks for which no time could be found, keyed by block title. + * @param {AutoTimeBlockingConfig} config - Configuration options for processing. + * @param {Array} newBlockList - The list of new blocks after processing. + * @param {Array} timeMap - The current time map. + * @returns {Object} - Returns the final result of matching tasks to slots, including unprocessed tasks handled as per config. + */ +export function handleUnprocessedTasks( + unprocessedTasks: Array, + noTimeForTasks: { [key: string]: Array }, + config: AutoTimeBlockingConfig, + newBlockList: Array, + timeMap: Array, +): TimeBlocksWithMap { + let finalUnprocessedTasks = unprocessedTasks || [] + const noTimeTasks = Object.values(noTimeForTasks)?.flat() || [] + clof(noTimeTasks, `handleUnprocessedTasks noTimeTasks=`, ['content', 'duration'], true) + + switch (config.orphanTagggedTasks) { + case 'IGNORE_THEM': + // If ignoring, do nothing further with noTimeTasks + break + case "OUTPUT_FOR_INFO (but don't schedule them)": + // If outputting for info, log or store these tasks separately (not shown here) + break + case 'SCHEDULE_ELSEWHERE_LAST': + finalUnprocessedTasks = [...finalUnprocessedTasks, ...noTimeTasks] + break + case 'SCHEDULE_ELSEWHERE_FIRST': + finalUnprocessedTasks = [...noTimeTasks, ...finalUnprocessedTasks] + break + } + + config.mode = 'PRIORITY_FIRST' + clof(finalUnprocessedTasks, `handleUnprocessedTasks finalUnprocessedTasks=`, 'content', true) + + return matchTasksToSlots(finalUnprocessedTasks, { blockList: newBlockList, timeMap }, config) +} + +/** + * Processes tasks by matching them to named time blocks based on tags. + * + * @param {Array} sortedTaskList - The list of tasks sorted by some criteria. + * @param {Array} blockList - The current list of time blocks. + * @param {Array} timeMap - The current time map. + * @param {Config} config - Configuration options for processing. + * @returns {Object} - Returns an object containing the updated block list, unprocessed tasks, results, and no time for tasks. + */ +export function processTasksByTimeBlockTag(sortedTaskList: Array, blockList: Array, timeMap: Array, config: AutoTimeBlockingConfig): Object { + let newBlockList = [...(blockList || [])] + let results = [] + let timeBlockTextList: any = [] + const noTimeForTasks = {} + + // MOVE THIS TO ITS OWN FUNCTION + // Split tasks into matched and unmatched based on tags + clo(config.timeframes, `processTasksByTimeBlockTag config.timeframes=`) + clo(blockList, `processTasksByTimeBlockTag blockList=`) + const { matched, unmatched } = splitItemsByTags(sortedTaskList, config.timeframes || {}) + let unprocessedTasks = unmatched || [] // tasks that do not match a timeframe will flow through to the next processing step + clof(matched, `processTasksByTimeBlockTag matched=`, null, true) + clof(unmatched, `processTasksByTimeBlockTag unmatched=`, ['content'], true) + const keys = Object.keys(matched) + if (keys.length) { + logDebug(`"STARTING TIMEFRAME PROCESSING": ${keys.length} timeframes matched in tasks`) + let newTimeMapWithBlocks = { timeBlockTextList: [], timeMap: [], blockList: [] } + // process tasks by timeframe key + keys.forEach((key) => { + const tasksMatchingThisTimeframe = matched[key] + const [start, end] = config.timeframes[key] + let timeMapCopy = deepCopy(timeMap) // timeMap.slice() + // process one task in the timeframe at a time + const sortedTasksMatchingTimeframe = sortListBy(tasksMatchingThisTimeframe, ['-priority', '-duration']) + sortedTasksMatchingTimeframe.forEach((task) => { + logDebug(`processTasksByTimeBlockTag TIMEFRAME:"${key}": start=${start} end=${end}`) + // blank out all slots that are not in the timeframe in question + timeMapCopy.forEach((t, i) => { + if (t.start < start || t.start >= end) { + timeMapCopy[i].busy = true // remove times from consideration that are not in the timeframe in question + } + }) + // filter the map to only open slots and then find open timeblocks + const openTimesForTimeframe = filterTimeMapToOpenSlots(timeMapCopy, config) + const blocksForTimeframe = findTimeBlocks(openTimesForTimeframe, config) + clof(blocksForTimeframe, `processTasksByTimeBlockTag blocksForTimeframe ${key} =`, ['start', 'minsAvailable'], true) + newTimeMapWithBlocks = matchTasksToSlots([task], { blockList: blocksForTimeframe, timeMap: openTimesForTimeframe }, config) + results.push(newTimeMapWithBlocks) + + const { timeMap: timeMapAfterTimeframePlacement, noTimeForTasks: nTftAfterTimeframePlacement, timeBlockTextList: timeBlockTextListAfterPlacement } = newTimeMapWithBlocks + timeMapCopy = timeMapAfterTimeframePlacement + // update the master timeMap with the changes that were made + // timemap slots that were used will be missing in the result, so we will just mark them as busy in the master timeMap + // function: updateMasterTimeMapWithTimeMapChanges + openTimesForTimeframe.forEach((t, i) => { + if (!timeMapAfterTimeframePlacement.find((nt) => nt.start === t.start)) { + ;(timeMap.find((tm) => tm.start === t.start) ?? {}).busy = true + } + }) + // save no time for tasks + if (nTftAfterTimeframePlacement) { + Object.keys(nTftAfterTimeframePlacement).forEach((key) => { + if (!noTimeForTasks[key]) noTimeForTasks[key] = [] + noTimeForTasks[key] = noTimeForTasks[key].concat(nTftAfterTimeframePlacement[key]) + }) + } + // save timeblocktextlist + if (timeBlockTextListAfterPlacement.length) { + timeBlockTextList = timeBlockTextList.concat(timeBlockTextListAfterPlacement) + } + //FIXME: I am here -- need to fix the circular dependency oy vey + }) + }) + } + clof(timeBlockTextList, `processTasksByTimeBlockTag timeBlockTextList=`, null, false) + clof(noTimeForTasks, `processTasksByTimeBlockTag noTimeForTasks=`, ['_', 'content'], true) + // end split + + logDebug(`"STARTING BY TIMEBLOCK TAG PROCESSING": ${unprocessedTasks.length} unprocessedTasks`) + clof(unprocessedTasks, `processTasksByTimeBlockTag unprocessedTasks=`, ['content'], true) + const filteredMap = filterTimeMapToOpenSlots(timeMap, config) + newBlockList = findTimeBlocks(filteredMap, config) + const namedBlocks = getNamedTimeBlocks(newBlockList ?? []) + namedBlocks.forEach((block) => { + const blockName = block.title || '' + logDebug(`PROCESSING BLOCK: "${blockName}" (tasks will be limited to this tag): ${unprocessedTasks.length} unprocessedTasks`) + const { + unprocessedTasks: updatedUnprocessedTasks, + results: blockResults, + noTimeForTasks: blockNoTimeForTasks, + } = processTasksForNamedTimeBlock(block, unprocessedTasks, timeMap, config) + clof(updatedUnprocessedTasks, `processTasksByTimeBlockTag updatedUnprocessedTasks after looking for block "${blockName}"`, null, true) + unprocessedTasks = updatedUnprocessedTasks + results = results.concat(blockResults) + Object.keys(blockNoTimeForTasks).forEach((key) => { + if (!noTimeForTasks[key]) noTimeForTasks[key] = [] + noTimeForTasks[key] = noTimeForTasks[key].concat(blockNoTimeForTasks[key]) + }) + + newBlockList = newBlockList.filter((b) => b !== block) + }) + + return { newBlockList, unprocessedTasks, results, noTimeForTasks } +} + +/** + * Processes the tasks that have a named tag in them (e.g., @work or #work) + * and schedules them within the specified time blocks. + * + * @param {Array} sortedTaskList - The list of tasks sorted by some criteria. + * @param {TimeBlockWithMap} tmb - The current time block with map. + * @param {Config} config - Configuration options for processing. + * @returns {TimeBlockWithMap} - The updated time block with map after processing. + */ +export function processByTimeBlockTag(sortedTaskList: Array, tmb: TimeBlocksWithMap, config: AutoTimeBlockingConfig): TimeBlocksWithMap { + // Destructure the initial time block with map structure + const { blockList, timeMap } = tmb + + // Process tasks by matching them to named time blocks based on tags + const { newBlockList, unprocessedTasks, results, noTimeForTasks } = processTasksByTimeBlockTag(sortedTaskList, blockList || [], timeMap, config) + + // Handle the unprocessed tasks according to the specified orphanTaggedTasks strategy + const unprocessedTasksResult = handleUnprocessedTasks(unprocessedTasks, noTimeForTasks, config, newBlockList, timeMap) + + // Combine results from named blocks processing and final processing + const combinedResults = [...results, unprocessedTasksResult] + const combinedNoTimeForTasks = { ...noTimeForTasks, ...unprocessedTasksResult.noTimeForTasks } + + // Prepare the final return structure + return { + noTimeForTasks: combinedNoTimeForTasks, + timeMap, + blockList: newBlockList, + timeBlockTextList: combinedResults.reduce((acc, currentValue) => acc.concat(currentValue.timeBlockTextList), []).sort(), + } +}