diff --git a/helpers/NPRequiredFiles.js b/helpers/NPRequiredFiles.js index e65ac192..8646d88b 100644 --- a/helpers/NPRequiredFiles.js +++ b/helpers/NPRequiredFiles.js @@ -22,9 +22,9 @@ export async function checkForRequiredSharedFiles(pluginJson: any): Promise { }) // New tests for handling double quotes inside the text + // FIXME(@dwertheimer): fails. JGC has looked and doesn't understand this particular case fully to decide whether to fix code or this test. test('should escape internal double quotes when wrapping with quotes', () => { const result = f.quoteText('foo "bar" baz') expect(result).toEqual('"foo \\"bar\\" baz"') // Inner double quotes are escaped diff --git a/helpers/__tests__/folders.test.js b/helpers/__tests__/folders.test.js index d50957dc..26f6617f 100644 --- a/helpers/__tests__/folders.test.js +++ b/helpers/__tests__/folders.test.js @@ -67,16 +67,19 @@ describe('helpers/folders', () => { const exclusions = ['CCC Areas'] const folders = Object.keys(f.getFoldersMatching([], false, exclusions)) expect(folders.length).toBe(7) + // expect(folders).toEqual(['@Templates', '/', 'CCC Projects', 'Home Areas', 'TEST', 'TEST/TEST LEVEL 2', 'TEST/TEST LEVEL 2/TEST LEVEL 3']) // doesn't work as expected, instead producing output ['0', '1','2','3', ...] }) test('exclude CCC, LEVEL 2; include @specials -> 4 left', () => { const exclusions = ['CCC', 'LEVEL 2'] const folders = Object.keys(f.getFoldersMatching([], false, exclusions)) expect(folders.length).toBe(4) + // expect(folders).toEqual(['@Templates', '/', 'Home Areas', 'TEST']) }) test('exclude CCC, LEVEL 2; no @specials -> 3 left', () => { const exclusions = ['CCC', 'LEVEL 2'] const folders = Object.keys(f.getFoldersMatching([], true, exclusions)) expect(folders.length).toBe(3) + // expect(folders).toEqual(['/', 'Home Areas', 'TEST']) }) }) describe('both inclusions + exclusions', () => { diff --git a/helpers/folders.js b/helpers/folders.js index 7bf07b88..b4a06498 100644 --- a/helpers/folders.js +++ b/helpers/folders.js @@ -3,7 +3,7 @@ // Folder-level Functions import { JSP, logDebug, logError, logInfo, logWarn } from './dev' -import { forceLeadingSlash } from '@helpers/general' +// import { forceLeadingSlash } from '@helpers/general' import { caseInsensitiveStartsWith, caseInsensitiveSubstringMatch } from './search' /** @@ -15,7 +15,7 @@ import { caseInsensitiveStartsWith, caseInsensitiveSubstringMatch } from './sear * Note: now clarified that this is a case-insensitive match. * @author @jgclark * @tests in jest file - * @param {Array} inclusions - if these (sub)strings match then exclude this folder -- can be empty + * @param {Array} inclusions - if not empty, use these (sub)strings to match folder items * @param {boolean} excludeSpecialFolders? * @param {Array} exclusions - if these (sub)strings match then exclude this folder. Optional: if none given then will treat as an empty list. * @returns {Array} array of folder names @@ -38,8 +38,8 @@ export function getFoldersMatching(inclusions: Array, excludeSpecialFold const rootExcluded = exclusions.some((f) => f === '/') // logDebug('getFoldersMatching', `- rootIncluded=${String(rootIncluded)}, rootExcluded=${String(rootExcluded)}`) const inclusionsWithoutRoot = inclusions.filter((f) => f !== '/') - // const exclusionsWithoutRoot = exclusions.filter((f) => f !== '/') - // logDebug('getFoldersMatching', `- inclusionsWithoutRoot=${String(inclusionsWithoutRoot)}, exclusionsWithoutRoot=${String(exclusionsWithoutRoot)}`) + const exclusionsWithoutRoot = exclusions.filter((f) => f !== '/') + // logDebug('getFoldersMatching', `- inclusionsWithoutRoot=${String(inclusionsWithoutRoot)} and exclusionsWithoutRoot=${String(exclusionsWithoutRoot)}`) // Deal with special case of inclusions just '/' if (inclusions.length === 1 && inclusions[0] === '/') { @@ -49,21 +49,26 @@ export function getFoldersMatching(inclusions: Array, excludeSpecialFold // if necessary filter fullFolderList to only folders that don't start with the character '@' (special folders) const reducedFolderList = excludeSpecialFolders ? fullFolderList.filter((folder) => !folder.startsWith('@')) : fullFolderList + // logDebug('folders / getFoldersMatching', `- after specials filter -> ${reducedFolderList.length} reducedFolderList: [${reducedFolderList.toString()}]`) // To aid partial matching, terminate all folder strings with a trailing / let reducedTerminatedWithSlash: Array = [] for (const f of reducedFolderList) { reducedTerminatedWithSlash.push(f.endsWith('/') ? f : `${f}/`) } - // logDebug('folders / getFoldersMatching', `- reduced ${reducedTerminatedWithSlash.length} folders: [${reducedTerminatedWithSlash.toString()}]`) + // logDebug('folders / getFoldersMatching', `- after termination -> ${reducedTerminatedWithSlash.length} reducedTWS:[${reducedTerminatedWithSlash.toString()}]`) - // filter reducedTerminatedWithSlash to only folders that start with an item in the inclusionsTerminatedWithSlash list. Note: now case insensitive. - reducedTerminatedWithSlash = reducedTerminatedWithSlash.filter((folder) => inclusionsWithoutRoot.some((f) => caseInsensitiveSubstringMatch(f, folder))) + // filter reducedTerminatedWithSlash to exclude items in the exclusions list (if non-empty). Note: now case insensitive. + if (exclusionsWithoutRoot.length > 0) { + reducedTerminatedWithSlash = reducedTerminatedWithSlash.filter((folder) => !exclusionsWithoutRoot.some((f) => caseInsensitiveSubstringMatch(f, folder))) + // logDebug('folders / getFoldersMatching',`- after exclusions -> ${reducedTerminatedWithSlash.length} reducedTWS: ${reducedTerminatedWithSlash.toString()}\n`) + } - // logDebug( - // 'folders / getFoldersMatching', - // `- after inclusions reducedTerminatedWithSlash: ${reducedTerminatedWithSlash.length} folders: ${reducedTerminatedWithSlash.toString()}\n`, - // ) + // filter reducedTerminatedWithSlash to only folders that start with an item in the inclusionsTerminatedWithSlash list (if non-empty). Note: now case insensitive. + if (inclusionsWithoutRoot.length > 0) { + reducedTerminatedWithSlash = reducedTerminatedWithSlash.filter((folder) => inclusionsWithoutRoot.some((f) => caseInsensitiveSubstringMatch(f, folder))) + // logDebug('folders / getFoldersMatching',`- after inclusions -> ${reducedTerminatedWithSlash.length} reducedTWS: ${reducedTerminatedWithSlash.toString()}\n`) + } // now remove trailing slash characters const outputList = reducedTerminatedWithSlash.map((folder) => (folder.endsWith('/') ? folder.slice(0, -1) : folder)) @@ -72,7 +77,7 @@ export function getFoldersMatching(inclusions: Array, excludeSpecialFold if (rootIncluded && !rootExcluded) { outputList.unshift('/') } - logDebug('getFoldersMatching', `-> outputList: ${outputList.length} items: [${outputList.toString()}]`) + // logDebug('getFoldersMatching', `-> outputList: ${outputList.length} items: [${outputList.toString()}]`) return outputList } catch (error) { logError('getFoldersMatching', error.message) diff --git a/helpers/paragraph.js b/helpers/paragraph.js index 40816db6..5e0de7e5 100644 --- a/helpers/paragraph.js +++ b/helpers/paragraph.js @@ -7,7 +7,9 @@ import { getDateStringFromCalendarFilename } from './dateTime' import { clo, logDebug, logError, logInfo, logWarn } from './dev' import { getElementsFromTask } from './sorting' import { RE_MARKDOWN_LINK_PATH_CAPTURE, RE_NOTELINK_G, RE_SIMPLE_URI_MATCH } from '@helpers/regex' +import { getLineMainContentPos } from '@helpers/search' import { stripLinksFromString } from '@helpers/stringTransforms' + //----------------------------------------------------------------------------- /** @@ -143,8 +145,8 @@ export function displayTitle(n: ?CoreNoteFields): string { return !n ? '(error)' : n.type === 'Calendar' - ? getDateStringFromCalendarFilename(n.filename) ?? '' // earlier: return n.filename.split('.')[0] // without file extension - : n.title ?? '(error)' + ? getDateStringFromCalendarFilename(n.filename) ?? '' // earlier: return n.filename.split('.')[0] // without file extension + : n.title ?? '(error)' } /** @@ -622,3 +624,26 @@ export function getTagsFromString(content: string, includeSymbol: boolean = true const mentions = getElementsFromTask(content, MENTIONS).map((tag) => (includeSymbol ? tag : tag.slice(1))) return { hashtags, mentions } } + + +/** + * Take a line and simplify by removing blockIDs, start-of-line markers, and trim start/end. + * Note: different from simplifyParaContent() which doesn't do as much. + * @author @jgclark + * @param {string} input + * @returns {string} simplified output + */ +export function simplifyRawContent(input: string): string { + try { + // Remove start-of-line markers + let output = input.slice(getLineMainContentPos(input)) + // Remove blockIDs (which otherwise can mess up the other sync'd copies) + output = output.replace(/\^[A-z0-9]{6}([^A-z0-9]|$)/g, '') + // Trim whitespace at start/end + output = output.trim() + return output + } catch (error) { + logError('simplifyRawContent', error.message) + return '' // for completeness + } +} diff --git a/jgclark.Dashboard/CHANGELOG.md b/jgclark.Dashboard/CHANGELOG.md index b2e6e192..5389df9c 100644 --- a/jgclark.Dashboard/CHANGELOG.md +++ b/jgclark.Dashboard/CHANGELOG.md @@ -1,9 +1,22 @@ # What's changed in 🎛 Dashboard plugin? For more details see the [plugin's documentation](https://github.com/NotePlan/plugins/tree/main/jgclark.Dashboard/). - +## [2.0.7] 2024-10-23 +### New +- new 'All -> Next Week' button in Week section. +- clicking on 'there are X items hidden' message lines now turns off filtering in all sections +- added version number to end of Settings dialog + +### Changed +- under-the-hood changes to match Project + Reviews Plugin v1.0 release. +- stop check dialogs on "Move all ..." operations on iOS/iPadOS, as they stopped them working +- changed Interactive Processing icon to not imply 'refresh' +- add time to @done(...) when "completing then" + +### Fixed +- fixed edge case when filtering lower priority items from the display +- fixed typos in "Move all to today" dialog + ## [2.0.6] 2024-09-06 ### Changes diff --git a/jgclark.Dashboard/plugin.json b/jgclark.Dashboard/plugin.json index 073d84cc..ab51b3d5 100644 --- a/jgclark.Dashboard/plugin.json +++ b/jgclark.Dashboard/plugin.json @@ -7,7 +7,7 @@ "plugin.author": "@jgclark", "plugin.version": "2.0.7", "plugin.hidden": false, - "plugin.lastUpdateInfo": "2.0.7: under-the-hood changes to match Project + Reviews Plugin v1.0 release.\n2.0.6: tweak to rescheduling items, and other small bug fixes and tweaks.\n2.0.5: fix bug in 'Move all overdue to Today' command.\n2.0.4: add new 'Priority' section.\n2.0.3: layout improvements\n2.0.2: small improvements.\n2.0.1: removal of older settings system: it now only uses the quick-access menu.\n2.0.0: major new release -- see documentation for all the new features", + "plugin.lastUpdateInfo": "2.0.7: new 'All -> Next Week' button in Week section. Under-the-hood changes to match Project + Reviews Plugin v1.0 release.\n2.0.6: tweak to rescheduling items, and other small bug fixes and tweaks.\n2.0.5: fix bug in 'Move all overdue to Today' command.\n2.0.4: add new 'Priority' section.\n2.0.3: layout improvements\n2.0.2: small improvements.\n2.0.1: removal of older settings system: it now only uses the quick-access menu.\n2.0.0: major new release -- see documentation for all the new features", "plugin.dependencies": [], "plugin.requiredFiles": [ "react.c.WebView.bundle.min.js", diff --git a/jgclark.Dashboard/src/dataGeneration.js b/jgclark.Dashboard/src/dataGeneration.js index 16078f9e..ae2ceb4c 100644 --- a/jgclark.Dashboard/src/dataGeneration.js +++ b/jgclark.Dashboard/src/dataGeneration.js @@ -1,13 +1,13 @@ // @flow //----------------------------------------------------------------------------- // Dashboard plugin main function to generate data -// Last updated 2024-09-27 for v2.0.6+ by @jgclark +// Last updated 2024-10-23 for v2.0.7 by @jgclark //----------------------------------------------------------------------------- import moment from 'moment/min/moment-with-locales' import pluginJson from '../plugin.json' import { Project } from '../../jgclark.Reviews/src/projectClass.js' -import { getNextProjectsToReview } from '../../jgclark.Reviews/src/reviewListHelpers.js' // assumes v0.15+ of Reviews Plugin +import { getNextProjectsToReview } from '../../jgclark.Reviews/src/allProjectsListHelpers.js' // assumes v0.15+ of Reviews Plugin import type { TDashboardSettings, TItemType, TParagraphForDashboard, TSectionCode, TSection, TSectionItem, TSectionDetails @@ -16,14 +16,12 @@ import { allSectionCodes } from "./constants.js" import { getTagSectionDetails } from './react/components/Section/sectionHelpers.js' import { getNumCompletedTasksTodayFromNote } from './countDoneTasks' import { - // extendParasToAddStartTimes, getDashboardSettings, getNotePlanSettings, getOpenItemParasForCurrentTimePeriod, getRelevantOverdueTasks, getRelevantPriorityTasks, getStartTimeFromPara, - // getSharedSettings, makeDashboardParas, } from './dashboardHelpers' import { @@ -48,36 +46,20 @@ import { getTodaysDateUnhyphenated, filenameIsInFuture, includesScheduledFutureDate, - // includesScheduledFutureDate, - // toISOShortDateTimeString, } from '@helpers/dateTime' import { clo, JSP, logDebug, logError, logInfo, logTimer, logWarn, timer } from '@helpers/dev' import { getFolderFromFilename } from '@helpers/folders' -// import { displayTitle } from '@helpers/general' -import { - // getTimeRangeFromTimeBlockString, - // localeDateStr, - toNPLocaleDateString, - // setMomentLocaleFromEnvironment, -} from '@helpers/NPdateTime' -import { - findNotesMatchingHashtagOrMention, - // getReferencedParagraphs -} from '@helpers/NPnote' +import { toNPLocaleDateString } from '@helpers/NPdateTime' +import { findNotesMatchingHashtagOrMention } from '@helpers/NPnote' import { sortListBy } from '@helpers/sorting' import { eliminateDuplicateSyncedParagraphs } from '@helpers/syncedCopies' -// import { getTimeBlockString } from '@helpers/timeblocks' -import { - // isOpen, isOpenTask, - isOpen, isOpenTask, - // removeDuplicates -} from '@helpers/utils' +import { isOpen, isOpenTask } from '@helpers/utils' //----------------------------------------------------------------- // Constants -const reviewPluginID = 'jgclark.Reviews' -const fullReviewListFilename = `../${reviewPluginID}/full-review-list.md` +// const reviewPluginID = 'jgclark.Reviews' +// const fullReviewListFilename = `../${reviewPluginID}/full-review-list.md` //----------------------------------------------------------------- @@ -704,6 +686,14 @@ export function getThisWeekSectionData(config: TDashboardSettings, useDemoData: display: ' ', actionParam: nextPeriodFilename, }, + { + actionName: 'moveAllThisWeekNextWeek', + actionPluginID: `${pluginJson["plugin.id"]}`, + tooltip: 'Move or schedule all open items from this week to next week', + display: 'All Next Week', + actionParam: 'true' /* refresh afterwards */, + postActionRefresh: ['W'] // refresh the week section afterwards + }, ], } sections.push(section) diff --git a/jgclark.Dashboard/src/moveClickHandlers.js b/jgclark.Dashboard/src/moveClickHandlers.js index be736a90..f69175ea 100644 --- a/jgclark.Dashboard/src/moveClickHandlers.js +++ b/jgclark.Dashboard/src/moveClickHandlers.js @@ -1,7 +1,7 @@ // @flow //----------------------------------------------------------------------------- // Dashboard plugin helper functions that need to refresh Dashboard -// Last updated 2024-09-06 for v2.0.6 by @dwertheimer +// Last updated 2024-10-23 for v2.0.7 by @jgclark //----------------------------------------------------------------------------- import moment from 'moment/min/moment-with-locales' @@ -19,10 +19,11 @@ import { type MessageDataObject, type TBridgeClickHandlerResult, } from './types' -import { clo, JSP, logDebug, logError, logInfo, logWarn, timer } from '@helpers/dev' +import { clo, JSP, logDebug, logError, logInfo, logWarn, logTimer } from '@helpers/dev' import { calcOffsetDateStr, getDateStringFromCalendarFilename, + getNPWeekStr, getTodaysDateHyphenated, getTodaysDateUnhyphenated, RE_DATE, @@ -135,7 +136,8 @@ export async function scheduleAllYesterdayOpenToToday(_data: MessageDataObject): // If there are lots, then double check whether to proceed // TODO: get this from newer settings instead - if (totalToMove > checkThreshold) { + // Note: platform limitation: can't run CommandBar from HTMLView on iOS/iPadOS + if (NotePlan.environment.platform === "macOS" && totalToMove > checkThreshold) { const res = await showMessageYesNo(`Are you sure you want to ${config.rescheduleNotMove ? 'schedule' : 'move'} ${totalToMove} items to today?`, ['Yes', 'No'], 'Move Yesterday to Today', false) if (res !== 'Yes') { logDebug('scheduleAllYesterdayOpenToToday', 'User cancelled operation.') @@ -145,7 +147,6 @@ export async function scheduleAllYesterdayOpenToToday(_data: MessageDataObject): let c = 0 if (combinedSortedParas.length > 0) { - // TODO: start a progress indicator reactWindowData.pluginData.refreshing = ['DT', 'DY'] await sendToHTMLWindow(WEBVIEW_WINDOW_ID, 'UPDATE_DATA', reactWindowData, `Refreshing JSON data for sections ${String(['DT', 'DY'])}`) @@ -223,7 +224,7 @@ export async function scheduleAllYesterdayOpenToToday(_data: MessageDataObject): DataStore.updateCache(thisNote, false) } } - logDebug('scheduleAllYesterdayOpenToToday', `-> scheduled ${String(numberScheduled)} open items from yesterday in project notes to today (in ${timer(thisStartTime)})`) + logTimer('scheduleAllYesterdayOpenToToday', thisStartTime, `-> scheduled ${String(numberScheduled)} open items from yesterday in project notes to today`) } else { logDebug('scheduleAllYesterdayOpenToToday', `- No ref paras for yesterday found`) } @@ -273,10 +274,10 @@ export async function scheduleAllTodayTomorrow(_data: MessageDataObject): Promis const [combinedSortedParas, sortedRefParas] = await getOpenItemParasForCurrentTimePeriod("day", todaysNote, config) const totalToMove = combinedSortedParas.length + sortedRefParas.length - // If there are lots, then double check whether to proceed // TODO: get this from newer settings instead - if (totalToMove > checkThreshold) { - const res = await showMessageYesNo(`Are you sure you want to ${config.rescheduleNotMove ? 'schedule' : 'nove'} ${totalToMove} items to tomorrow?`, ['Yes', 'No'], 'Move Yesterday to Today', false) + // Note: platform limitation: can't run CommandBar from HTMLView on iOS/iPadOS + if (NotePlan.environment.platform === "macOS" && totalToMove > checkThreshold) { + const res = await showMessageYesNo(`Are you sure you want to ${config.rescheduleNotMove ? 'schedule' : 'move'} ${totalToMove} items to tomorrow?`, ['Yes', 'No'], 'Move Today to Tomorrow', false) if (res !== 'Yes') { logDebug('scheduleAllTodayTomorrow', 'User cancelled operation.') return { success: false } @@ -285,7 +286,6 @@ export async function scheduleAllTodayTomorrow(_data: MessageDataObject): Promis let c = 0 if (combinedSortedParas.length > 0) { - // TODO: start a progress indicator reactWindowData.pluginData.refreshing = ['DT', 'DO'] await sendToHTMLWindow(WEBVIEW_WINDOW_ID, 'UPDATE_DATA', reactWindowData, `Refreshing JSON data for sections ${String(['DT', 'DO'])}`) @@ -319,7 +319,7 @@ export async function scheduleAllTodayTomorrow(_data: MessageDataObject): Promis logWarn('scheduleAllTodayTomorrow', `-> moveFromCalToCal from {todayDateStr} to ${tomorrowDateStr} not successful`) } } - logDebug('scheduleAllTodayTomorrow', `moved ${String(numberScheduled)} open items from today to tomorrow's note (in ${timer(thisStartTime)})`) + logTimer('scheduleAllTodayTomorrow', thisStartTime, `moved ${String(numberScheduled)} open items from today to tomorrow's note`) // Update cache to allow it to be re-read on refresh DataStore.updateCache(todaysNote, false) } @@ -370,6 +370,138 @@ export async function scheduleAllTodayTomorrow(_data: MessageDataObject): Promis } } +/** + * Function to schedule or move all open items from this week to next week. + * Uses config setting 'rescheduleNotMove' to decide whether to reschedule or move. + * @param {MessageDataObject} data + * @returns {TBridgeClickHandlerResult} + */ +export async function scheduleAllThisWeekNextWeek(_data: MessageDataObject): Promise { + try { + + let numberScheduled = 0 + const config = await getDashboardSettings() + // Override one config item so we can work on separate dated vs scheduled items + config.separateSectionForReferencedNotes = true + const thisStartTime = new Date() + const today = new moment().toDate() // use moment instead of `new Date` to ensure we get a date in the local timezone + const reactWindowData = await getGlobalSharedData(WEBVIEW_WINDOW_ID) + + // Get paras for all open items in yesterday's note + // TODO: get this from reactWindowData.pluginData instead + const thisWeekDateStr = getNPWeekStr(today) + const nextWeekDateStr = calcOffsetDateStr(thisWeekDateStr, '1w') + const thisWeekNote = DataStore.calendarNoteByDate(today, 'week') + logDebug('scheduleAllThisWeekNextWeek', `Starting with this week's note ${thisWeekDateStr} -> ${nextWeekDateStr}`) + + if (!thisWeekNote) { + logWarn('scheduleAllThisWeekNextWeek', `Oddly I can't find a weekly note for today (${thisWeekDateStr})`) + return { success: false } + } else { + logDebug('scheduleAllThisWeekNextWeek', `Starting with this week's note${thisWeekDateStr}`) + } + + // Get list of open tasks/checklists from this calendar note + const [combinedSortedParas, sortedRefParas] = await getOpenItemParasForCurrentTimePeriod("week", thisWeekNote, config) + const totalToMove = combinedSortedParas.length + sortedRefParas.length + + // If there are lots, then double check whether to proceed + // TODO: get this from newer settings instead + // Note: platform limitation: can't run CommandBar from HTMLView on iOS/iPadOS + if (NotePlan.environment.platform === "macOS" && totalToMove > checkThreshold) { + const res = await showMessageYesNo(`Are you sure you want to ${config.rescheduleNotMove ? 'schedule' : 'move'} ${totalToMove} items to next week?`, ['Yes', 'No'], 'Move This Week to Next Week', false) + if (res !== 'Yes') { + logDebug('scheduleAllThisWeekNextWeek', 'User cancelled operation.') + return { success: false } + } + } + + let c = 0 + if (combinedSortedParas.length > 0) { + reactWindowData.pluginData.refreshing = ['W'] + await sendToHTMLWindow(WEBVIEW_WINDOW_ID, 'UPDATE_DATA', reactWindowData, `Refreshing JSON data for section ['W']`) + + if (config.rescheduleNotMove) { + // For each para append ' >' and next week's ISO date + for (const dashboardPara of combinedSortedParas) { + c++ + // CommandBar.showLoading(true, `Scheduling item ${c} to next week`, c / totalToMove) + logDebug('scheduleAllThisWeekNextWeek', `- scheduling "${dashboardPara.content}" to next week`) + // Convert each reduced para back to the full one to update + const p = getParagraphFromStaticObject(dashboardPara) + if (p) { + p.content = replaceArrowDatesInString(p.content, `>${nextWeekDateStr}`) + p.note?.updateParagraph(p) + DataStore.updateCache(p.note, false) + numberScheduled++ + } + } + logDebug('scheduleAllThisWeekNextWeek', `scheduled ${String(numberScheduled)} open items from this week's note`) + } else { + // For each para move to next week's note + for (const para of combinedSortedParas) { + logDebug('scheduleAllThisWeekNextWeek', `- moving "${para.content}" to next week`) + c++ + // CommandBar.showLoading(true, `Moving item ${c} to next week`, c / totalToMove) + const res = await moveItemBetweenCalendarNotes(thisWeekDateStr, nextWeekDateStr, para.content, config.newTaskSectionHeading ?? '') + if (res) { + logDebug('scheduleAllThisWeekNextWeek', `-> appeared to move item succesfully`) + numberScheduled++ + } else { + logWarn('scheduleAllThisWeekNextWeek', `-> moveFromCalToCal from {this weekDateStr} to ${nextWeekDateStr} not successful`) + } + } + logTimer('scheduleAllThisWeekNextWeek', thisStartTime, `moved ${String(numberScheduled)} open items from this week to next week's note`) + // Update cache to allow it to be re-read on refresh + DataStore.updateCache(thisWeekNote, false) + } + } + + // Now do the same for items scheduled to this week from other notes + if (sortedRefParas.length > 0) { + reactWindowData.pluginData.refreshing = ['W'] + await sendToHTMLWindow(WEBVIEW_WINDOW_ID, 'UPDATE_DATA', reactWindowData, `Refreshing JSON data for sections ['W']`) + + // For each para append ' >YYYY-Wnn' + for (const dashboardPara of sortedRefParas) { + c++ + // CommandBar.showLoading(true, `Scheduling item ${c} to tomorrow`, c / totalToMove) + const thisNote = DataStore.noteByFilename(dashboardPara.filename, dashboardPara.noteType) + if (!thisNote) { + logWarn('scheduleAllThisWeekNextWeek', `Oddly I can't find the note for "${dashboardPara.content}", so can't process this item`) + } else { + // Convert each reduced para back to the full one to update. + const p = getParagraphFromStaticObject(dashboardPara) + if (p) { + p.content = replaceArrowDatesInString(p.content, `>${nextWeekDateStr}`) + logDebug('scheduleAllThisWeekNextWeek', `- scheduling referenced para "${p.content}" from note ${thisNote.filename}`) + thisNote.updateParagraph(p) + } else { + logWarn('scheduleAllYesterdayOpenToThis week', `Couldn't find para matching "${dashboardPara.content}"`) + } + + numberScheduled++ + // Update cache to allow it to be re-read on refresh + DataStore.updateCache(thisNote, false) + } + } + logDebug('scheduleAllThisWeekNextWeek', `-> scheduled ${String(numberScheduled)} open items from this week to tomorrow`) + } + + // remove progress indicator + reactWindowData.pluginData.refreshing = false + await sendToHTMLWindow(WEBVIEW_WINDOW_ID, 'UPDATE_DATA', reactWindowData, `scheduleAllYesterdayOpenToThis week finished `) + + // Update display of these 2 sections + logDebug('scheduleAllThisWeekNextWeek', `returning {true, REFRESH_SECTION_IN_JSON, [W]}`) + return { success: true, actionsOnSuccess: ['REFRESH_SECTION_IN_JSON', 'START_DELAYED_REFRESH_TIMER'], sectionCodes: ['W'] } + } + catch (error) { + logError('dashboard / scheduleAllThisWeekNextWeek', error.message) + return { success: false } + } +} + //----------------------------------------------------------------- /** * Function to schedule or move all open overdue tasks from their notes to today @@ -415,12 +547,14 @@ export async function scheduleAllOverdueOpenToToday(_data: MessageDataObject): P logInfo('scheduleAllOverdueOpenToToday', `Can't find any overdue items; this can happen if all were from yesterday, and have been de-duped. Stopping.`) return { success: false } } - logInfo('scheduleAllOverdueOpenToToday', `Found ${totalOverdue} overdue items to ${config.rescheduleNotMove ? 'rescheduleItem' : 'move'} to today (in ${timer(thisStartTime)})`) + logTimer('scheduleAllOverdueOpenToToday', thisStartTime, `Found ${totalOverdue} overdue items to ${config.rescheduleNotMove ? 'rescheduleItem' : 'move'} to today`) const todayDateStr = getTodaysDateHyphenated() // If there are lots, then double check whether to proceed - if (totalOverdue > checkThreshold) { + // TODO: get this from newer settings instead + // Note: platform limitation: can't run CommandBar from HTMLView on iOS/iPadOS + if (NotePlan.environment.platform === "macOS" && totalOverdue > checkThreshold) { const res = await showMessageYesNo(`Are you sure you want to ${config.rescheduleNotMove ? 'rescheduleItem' : 'move'} ${totalOverdue} overdue items to today? This can be a slow operation, and can't easily be undone.`, ['Yes', 'No'], 'Move Overdue to Today', false) if (res !== 'Yes') { logDebug('scheduleAllOverdueOpenToToday', 'User cancelled operation.') @@ -488,7 +622,7 @@ export async function scheduleAllOverdueOpenToToday(_data: MessageDataObject): P // Update cache to allow it to be re-read on refresh DataStore.updateCache(thisNote, false) } - logDebug('scheduleAllOverdueOpenToToday', `moved ${String(numberChanged)} overdue items to today's note (in ${timer(thisStartTime)})`) + logTimer('scheduleAllOverdueOpenToToday', thisStartTime, `moved ${String(numberChanged)} overdue items to today's note`) } } else { throw new Error(`after finding ${String(totalOverdue)} overdue items to move/reschedule a little earlier, I now don't have any!`) diff --git a/jgclark.Dashboard/src/pluginToHTMLBridge.js b/jgclark.Dashboard/src/pluginToHTMLBridge.js index 8c586955..7b465901 100644 --- a/jgclark.Dashboard/src/pluginToHTMLBridge.js +++ b/jgclark.Dashboard/src/pluginToHTMLBridge.js @@ -1,7 +1,7 @@ // @flow //----------------------------------------------------------------------------- // Bridging functions for Dashboard plugin -// Last updated 2024-09-06 for v2.0.6 by @jgclark +// Last updated 2024-10-23 for v2.0.7 by @jgclark //----------------------------------------------------------------------------- import pluginJson from '../plugin.json' @@ -44,6 +44,7 @@ import { import { doMoveFromCalToCal, scheduleAllOverdueOpenToToday, + scheduleAllThisWeekNextWeek, scheduleAllTodayTomorrow, scheduleAllYesterdayOpenToToday, } from './moveClickHandlers' @@ -279,6 +280,10 @@ export async function bridgeClickDashboardItem(data: MessageDataObject) { result = await scheduleAllOverdueOpenToToday(data) break } + case 'moveAllThisWeekNextWeek': { + result = await scheduleAllThisWeekNextWeek(data) + break + } default: { logWarn('bridgeClickDashboardItem', `bridgeClickDashboardItem: can't yet handle type ${actionType}`) } diff --git a/jgclark.Dashboard/src/react/components/ItemRow.jsx b/jgclark.Dashboard/src/react/components/ItemRow.jsx index 02f7b3b8..a42c5cc5 100644 --- a/jgclark.Dashboard/src/react/components/ItemRow.jsx +++ b/jgclark.Dashboard/src/react/components/ItemRow.jsx @@ -31,7 +31,7 @@ function ItemRow({ item, thisSection }: Props): React.Node { ) : itemType === 'filterIndicator' ? ( - ) : itemType === 'congrats' ? ( + ) : itemType === 'itemCongrats' ? ( ) : ( diff --git a/jgclark.Dashboard/src/react/components/RefreshControl.jsx b/jgclark.Dashboard/src/react/components/RefreshControl.jsx index 4385f0bd..8408a57e 100644 --- a/jgclark.Dashboard/src/react/components/RefreshControl.jsx +++ b/jgclark.Dashboard/src/react/components/RefreshControl.jsx @@ -1,8 +1,9 @@ // @flow - +//---------------------------------------------------------------------- // RefreshControl.jsx // renders a refresh button or a refreshing spinner depending on refreshing state -// Last updated 2024-06-25 for v2.0.0-b14 by @jgclark +// Last updated 2024-10-23 for v2.0.7 by @jgclark +//---------------------------------------------------------------------- import React from 'react' import Button from './Button.jsx' @@ -26,8 +27,8 @@ const RefreshControl = (props: Props): React$Node => { )} diff --git a/jgclark.Dashboard/src/react/components/Section/useSectionSortAndFilter.jsx b/jgclark.Dashboard/src/react/components/Section/useSectionSortAndFilter.jsx index e33e77e4..588625b0 100644 --- a/jgclark.Dashboard/src/react/components/Section/useSectionSortAndFilter.jsx +++ b/jgclark.Dashboard/src/react/components/Section/useSectionSortAndFilter.jsx @@ -1,12 +1,12 @@ // @flow //----------------------------------------------------------------------------- // useSectionSortAndFilter.jsx -// Last updated 2024-07-16 for v2.0.2 by @jgclark +// Last updated 2024-10-24 for v2.0.7 by @jgclark //----------------------------------------------------------------------------- import { useState, useEffect } from 'react' import type { TSection, TSectionItem } from '../../../types.js' -import { logDebug, logError, JSP, clo } from '@helpers/react/reactDev' +import { logDebug, logError, logInfo, JSP, clo } from '@helpers/react/reactDev' type UseSectionSortAndFilter = { filteredItems: Array, @@ -28,16 +28,23 @@ const useSectionSortAndFilter = ( useEffect(() => { const filterPriorityItems = dashboardSettings?.filterPriorityItems ?? false const limitToApply = dashboardSettings?.maxItemsToShowInSection ?? 20 - let maxPrioritySeen = 0 + + // Find highest priority seen + let maxPrioritySeen = -1 for (const i of items) { if (i.para?.priority && i.para.priority > maxPrioritySeen) { maxPrioritySeen = i.para.priority } } - - const filteredItems = filterPriorityItems ? items.filter(f => (f.para?.priority ?? 0) >= maxPrioritySeen) : items.slice() + logInfo('useSectionSortAndFilter', `Highest priority seen: ${maxPrioritySeen} with ${items.length} items`) + // and then filter lower-priority items (if wanted) + const filteredItems = filterPriorityItems + ? items.filter(f => (f.para?.priority ?? 0) >= maxPrioritySeen) + : items.slice() const priorityFilteringHappening = items.length > filteredItems.length + logInfo('useSectionSortAndFilter', `After filter: ${String(priorityFilteringHappening)} and ${filteredItems.length} items`) + // sort items filteredItems.sort((a, b) => { if (a.para?.startTime && b.para?.startTime) { const startTimeComparison = a.para.startTime.localeCompare(b.para.startTime) diff --git a/jgclark.Dashboard/src/react/components/SettingsDialog.jsx b/jgclark.Dashboard/src/react/components/SettingsDialog.jsx index ed3861d9..ced272e2 100644 --- a/jgclark.Dashboard/src/react/components/SettingsDialog.jsx +++ b/jgclark.Dashboard/src/react/components/SettingsDialog.jsx @@ -3,7 +3,7 @@ // Dashboard React component to show the settings dialog // Changes are saved when "Save & Close" is clicked, but not before // Called by Header component. -// Last updated 2024-09-06 for v2.0.6 by @jgclark +// Last updated 2024-10-24 for v2.0.7 by @jgclark //-------------------------------------------------------------------------- //-------------------------------------------------------------------------- @@ -51,7 +51,9 @@ const SettingsDialog = ({ //---------------------------------------------------------------------- // Context //---------------------------------------------------------------------- - const { dashboardSettings, setDashboardSettings } = useAppContext() + const { dashboardSettings, setDashboardSettings, pluginData } = useAppContext() + + const pluginDisplayVersion = `v${pluginData.version}` //---------------------------------------------------------------------- // State @@ -188,6 +190,7 @@ const SettingsDialog = ({ )} ))} +
{pluginDisplayVersion}
diff --git a/jgclark.Dashboard/src/react/components/StatusIcon.jsx b/jgclark.Dashboard/src/react/components/StatusIcon.jsx index 4178e2c9..ba083d98 100644 --- a/jgclark.Dashboard/src/react/components/StatusIcon.jsx +++ b/jgclark.Dashboard/src/react/components/StatusIcon.jsx @@ -50,7 +50,7 @@ const StatusIcon = ({ return 'todo fa-regular fa-square' case 'checklistCancelled': return 'cancelled fa-regular fa-square-xmark' - case 'congrats': + case 'itemCongrats': return 'fa-regular fa-circle-check' case 'deleted': return 'fa-regular fa-trash-xmark' diff --git a/jgclark.Dashboard/src/react/components/TasksFiltered.jsx b/jgclark.Dashboard/src/react/components/TasksFiltered.jsx index ad4ca21c..775a25a1 100644 --- a/jgclark.Dashboard/src/react/components/TasksFiltered.jsx +++ b/jgclark.Dashboard/src/react/components/TasksFiltered.jsx @@ -2,12 +2,13 @@ //-------------------------------------------------------------------------- // Dashboard React component to show an Indicator that a Filter has been applied and so some item(s) have been hidden. // Called by ItemRow component -// Last updated 2024-07-14 for v2.0.2 by @jgclark -// TODO: be smarter about what to do if this line is clicked on. Or stop it being a link. +// Last updated 2024-10-23 for v2.0.7 by @jgclark //-------------------------------------------------------------------------- -import * as React from 'react' +import React, { type Node } from 'react' import type { TSectionItem } from '../../types.js' +import { useAppContext } from './AppContext.jsx' +import { clo, logDebug, logWarn } from '@helpers/react/reactDev.js' type Props = { item: TSectionItem, @@ -16,7 +17,14 @@ type Props = { /** * Component for displaying a filter indicator. */ -const TasksFiltered = ({ item }: Props): React.Node => { +const TasksFiltered = ({ item }: Props): Node => { + const { /*sendActionToPlugin, */ setDashboardSettings } = useAppContext() + + function handleLineClick(_e: MouseEvent) { + // logDebug('TasksFiltered/handleLineClick', `Trying to update filterPriorityItems setting`) + setDashboardSettings(prevSettings => ({ ...prevSettings, ['filterPriorityItems']: false })) + } + return (
{/* This empty span needed to mimic the StatusIcon line */} @@ -25,7 +33,9 @@ const TasksFiltered = ({ item }: Props): React.Node => {
-
+
handleLineClick(e)} > {item?.para?.content || ''} diff --git a/jgclark.Dashboard/src/reactMain.js b/jgclark.Dashboard/src/reactMain.js index 9ab1f7bd..6a6f3e54 100644 --- a/jgclark.Dashboard/src/reactMain.js +++ b/jgclark.Dashboard/src/reactMain.js @@ -1,10 +1,9 @@ // @flow //----------------------------------------------------------------------------- // Dashboard plugin main file (for React v2.0.0+) -// Last updated 2024-07-13 for v2.0.1 by @jgclark +// Last updated 2024-10-24 for v2.0.7 by @jgclark //----------------------------------------------------------------------------- -// import moment from 'moment/min/moment-with-locales' import pluginJson from '../plugin.json' import type { TPluginData, TDashboardSettings } from './types' import { allSectionDetails, WEBVIEW_WINDOW_ID } from "./constants" @@ -19,7 +18,6 @@ import { getGlobalSharedData, sendToHTMLWindow, sendBannerMessage } from '@helpers/HTMLView' -// import { toNPLocaleDateString } from '@helpers/NPdateTime' import { checkForRequiredSharedFiles } from '@helpers/NPRequiredFiles' import { generateCSSFromTheme } from '@helpers/NPThemeToCSS' import { getWindowFromId } from '@helpers/NPWindows' @@ -326,6 +324,7 @@ export async function getInitialDataForReactWindow(dashboardSettings: TDashboard demoMode: useDemoData, platform: NotePlan.environment.platform, // used in dialog positioning themeName: dashboardSettings.dashboardTheme ? dashboardSettings.dashboardTheme : Editor.currentTheme?.name || '', + version: pluginJson["plugin.version"], } // Calculate all done task counts (if the appropriate setting is on) diff --git a/jgclark.Dashboard/src/types.js b/jgclark.Dashboard/src/types.js index 2f4a00d1..bad7a0d5 100644 --- a/jgclark.Dashboard/src/types.js +++ b/jgclark.Dashboard/src/types.js @@ -1,7 +1,7 @@ // @flow //----------------------------------------------------------------------------- // Types for Dashboard code -// Last updated 2024-09-06 for v2.0.6 by @jgclark +// Last updated 2024-10-24 for v2.0.7 by @jgclark //----------------------------------------------------------------------------- // Types for Settings @@ -91,7 +91,7 @@ export type TSection = { doneCounts?: TDoneCount, // number of tasks and checklists completed today etc. } -export type TItemType = 'open' | 'checklist' | 'congrats' | 'project' | 'filterIndicator' +export type TItemType = 'open' | 'checklist' | 'itemCongrats' | 'project' | 'filterIndicator' // an item within a section, with optional TParagraphForDashboard export type TSectionItem = { @@ -155,6 +155,7 @@ export type TActionType = | 'cyclePriorityStateUp' | 'cyclePriorityStateDown' | 'deleteItem' + | 'moveAllThisWeekNextWeek' | 'moveAllTodayToTomorrow' | 'moveAllYesterdayToToday' | 'moveFromCalToCal' @@ -270,6 +271,7 @@ export type TPluginData = { demoMode: boolean, /* use fake content for demo purposes */ totalDoneCounts?: TDoneCount, startDelayedRefreshTimer?: boolean, /* start the delayed refresh timer hack set in post processing commands*/ + version: string, } export type TSettingItemType = 'switch' | 'input' | 'combo' | 'number' | 'text' | 'separator' | 'heading' | 'header'