{
try {
+ const useDemoData = config.useDemoData ?? false
if (config.projectTypeTags.length === 0) {
throw new Error('No projectTypeTags configured to display')
}
- if (!shouldOpen && !isHTMLWindowOpen(customRichWinId)) {
+ const richWinId = useDemoData ? customRichWinIdDemo : customRichWinId
+ if (!shouldOpen && !isHTMLWindowOpen(richWinId)) {
logDebug('renderProjectListsHTML', `not continuing, as HTML window isn't open and 'shouldOpen' is false.`)
return
}
const funcTimer = new moment().toDate() // use moment instead of `new Date` to ensure we get a date in the local timezone
logInfo(pluginJson, `renderProjectLists ----------------------------------------`)
- logDebug('renderProjectListsHTML', `Starting for ${String(config.projectTypeTags)} tags`)
+ logDebug('renderProjectListsHTML', `Starting for ${String(config.projectTypeTags)} tags${useDemoData ? ' (demo)' : ''}`)
// Test to see if we have the font resources we want
const res = await checkForWantedResources(pluginID)
@@ -501,59 +330,99 @@ export async function renderProjectListsHTML(
// Ensure projectTypeTags is an array before proceeding
if (typeof config.projectTypeTags === 'string') config.projectTypeTags = [config.projectTypeTags]
+ // Fetch project list first so we can compute per-tag active counts for the Filters dropdown
+ const [projectsToReview, _numberProjectsUnfiltered] = await filterAndSortProjectsList(config, '', [], true, useDemoData)
+ const wantedTags = config.projectTypeTags ?? []
+ const tagActiveCounts = wantedTags.map((tag) =>
+ projectsToReview.filter(
+ (p) =>
+ !p.isPaused &&
+ !p.isCancelled &&
+ !p.isCompleted &&
+ p.allProjectTags != null &&
+ p.allProjectTags.includes(tag)
+ ).length
+ )
+ config.tagActiveCounts = tagActiveCounts
+
// String array to save all output
const outputArray = []
- // Generate top bar HTML
- outputArray.push(generateTopBarHTML(config))
+ // Generate top bar HTML (uses config.tagActiveCounts for dropdown tag counts)
+ outputArray.push(buildProjectListTopBarHtml(config))
// Start multi-col working (if space)
outputArray.push(``)
logTimer('renderProjectListsHTML', funcTimer, `before main loop`)
-
- // Make the Summary list, for each projectTag in turn
- for (const thisTag of config.projectTypeTags) {
- // Get the summary line for each revelant project
- const [thisSummaryLines, noteCount, due] = await generateReviewOutputLines(thisTag, 'Rich', config)
-
- // Generate project tag section header
- outputArray.push(generateHTMLForProjectTagSectionHeader(thisTag, noteCount, due, config, config.projectTypeTags.length > 1))
-
- if (noteCount > 0) {
- outputArray.push(generateTableStructureHTML(config, noteCount))
- outputArray.push(thisSummaryLines.join('\n'))
- outputArray.push('
')
- outputArray.push(' ') // details-content div
- if (config.projectTypeTags.length > 1) {
- outputArray.push(``)
+ const noteCount = projectsToReview.length
+ outputArray.push(buildProjectListGridPrefixHtml(config))
+ if (useDemoData && noteCount === 0) {
+ outputArray.push('Demo file (allProjectsDemoList.json) not found or empty.
')
+ }
+ if (noteCount > 0) {
+ let lastFolder = ''
+ for (const thisProject of projectsToReview) {
+ if (!useDemoData) {
+ const thisNote = DataStore.projectNoteByFilename(thisProject.filename)
+ if (!thisNote) {
+ logWarn('renderProjectListsHTML', `Can't find note for filename ${thisProject.filename}`)
+ continue
+ }
}
+ if (config.displayGroupedByFolder && lastFolder !== thisProject.folder) {
+ const folderDisplayName = getFolderDisplayNameForHTML(thisProject.folder)
+ let folderPart = folderDisplayName
+ if (config.hideTopLevelFolder) {
+ if (folderDisplayName.includes(']')) {
+ const match = folderDisplayName.match(/^(\[.*?\])\s*(.+)$/)
+ if (match) {
+ const pathPart = match[2]
+ const pathParts = pathPart.split('/').filter(p => p !== '')
+ folderPart = `${match[1]} ${pathParts.length > 0 ? pathParts[pathParts.length - 1] : pathPart}`
+ } else {
+ folderPart = folderDisplayName.split('/').slice(-1)[0] || folderDisplayName
+ }
+ } else {
+ const pathParts = folderDisplayName.split('/').filter(p => p !== '')
+ folderPart = pathParts.length > 0 ? pathParts[pathParts.length - 1] : folderDisplayName
+ }
+ }
+ if (thisProject.folder === '/') folderPart = '(root folder)'
+ outputArray.push(buildFolderGroupHeaderHtml(folderPart, config))
+ }
+ const wantedTagsForRow = (thisProject.allProjectTags != null && wantedTags.length > 0)
+ ? thisProject.allProjectTags.filter(t => wantedTags.includes(t))
+ : []
+ outputArray.push(buildProjectLineForStyle(thisProject, config, 'Rich', wantedTagsForRow))
+ lastFolder = thisProject.folder
}
- logTimer('renderProjectListsHTML', funcTimer, `end of loop for ${thisTag}`)
+ outputArray.push(' ')
}
+ logTimer('renderProjectListsHTML', funcTimer, `end single section (${noteCount} projects)`)
// Generate project control dialog HTML
- outputArray.push(generateProjectControlDialogHTML())
+ outputArray.push(buildProjectControlDialogHtml())
const body = outputArray.join('\n')
logTimer('renderProjectListsHTML', funcTimer, `end of main loop`)
const setScrollPosJS: string = `
`
const winOptions = {
- windowTitle: windowTitle,
- customId: customRichWinId,
- headerTags: `${faLinksInHeader}${stylesheetinksInHeader}\n`,
+ windowTitle: useDemoData ? windowTitleDemo : windowTitle,
+ customId: richWinId,
+ headerTags: `${faLinksInHeader}${stylesheetinksInHeader}\n\n`,
generalCSSIn: generateCSSFromTheme(config.reviewsTheme), // either use dashboard-specific theme name, or get general CSS set automatically from current theme
- specificCSS: '', // now in requiredFiles/reviewListCSS instead
+ specificCSS: '', // now in requiredFiles/projectList.css instead
makeModal: false, // = not modal window
bodyOptions: 'onload="showTimeAgo()"',
preBodyScript: setPercentRingJSFunc + scrollPreLoadJSFuncs,
- postBodyScript: checkboxHandlerJSFunc + setScrollPosJS + displayFiltersDropdownScript + `
+ postBodyScript: checkboxHandlerJSFunc + setScrollPosJS + displayFiltersDropdownScript + tagTogglesVisibilityScript + autoRefreshScript + `
` + commsBridgeScripts + shortcutsScript + addToggleEvents, // + collapseSection + resizeListenerScript + unloadListenerScript,
@@ -570,7 +439,7 @@ export async function renderProjectListsHTML(
iconColor: pluginJson['plugin.iconColor'],
autoTopPadding: true,
showReloadButton: true,
- reloadCommandName: 'displayProjectLists',
+ reloadCommandName: useDemoData ? 'displayProjectListsDemo' : 'displayProjectLists',
reloadPluginID: 'jgclark.Reviews',
}
const thisWindow = await showHTMLV2(body, winOptions)
@@ -789,7 +658,7 @@ export async function generateReviewOutputLines(projectTag: string, style: strin
continue
}
// Make the output line for this project
- const out = generateProjectOutputLine(thisProject, config, style)
+ const out = buildProjectLineForStyle(thisProject, config, style)
// Add to number of notes to review (if appropriate)
if (!thisProject.isPaused && thisProject.nextReviewDays != null && !isNaN(thisProject.nextReviewDays) && thisProject.nextReviewDays <= 0) {
@@ -831,7 +700,7 @@ export async function generateReviewOutputLines(projectTag: string, style: strin
// Handle root folder display - check if original folder was root, not the display name
if (folder === '/') folderPart = '(root folder)'
if (style.match(/rich/i)) {
- outputArray.push(generateFolderHeaderHTML(folderPart, config))
+ outputArray.push(buildFolderGroupHeaderHtml(folderPart, config))
} else if (style.match(/markdown/i)) {
outputArray.push(`### ${folderPart}`)
}
@@ -888,20 +757,30 @@ async function finishReviewCoreLogic(note: CoreNoteFields): Promise {
}
const possibleThisEditor = getOpenEditorFromFilename(note.filename)
- if (possibleThisEditor) {
- logDebug('finishReviewCoreLogic', `Updating Editor '${displayTitle(possibleThisEditor)}' ...`)
- // First update @review(date) on current open note
- updateMetadataInEditor(possibleThisEditor, [reviewedTodayString])
+ if (possibleThisEditor && possibleThisEditor.note != null) {
+ logDebug('finishReviewCoreLogic', `Updating EDITOR note '${displayTitle(possibleThisEditor.note)}' ...`)
+ // If project metadata is in frontmatter, replace any body metadata line with migration message (or remove that message)
+ // before we recalculate the metadata line index and update mentions. This ensures that when both frontmatter and
+ // body metadata are present, we first migrate/merge them and then clean up @nextReview/@reviewed mentions once.
+ migrateProjectMetadataLineInEditor(possibleThisEditor)
+ const metadataLineIndex: number = getOrMakeMetadataLineIndex(possibleThisEditor)
// Remove a @nextReview(date) if there is one, as that is used to skip a review, which is now done.
- deleteMetadataMentionInEditor(possibleThisEditor, [config.nextReviewMentionStr])
+ deleteMetadataMentionInEditor(possibleThisEditor, metadataLineIndex, [config.nextReviewMentionStr])
+ // Update @review(date) on current open note
+ updateMetadataInEditor(possibleThisEditor, [reviewedTodayString])
await possibleThisEditor.save()
// Note: no longer seem to need to update cache
} else {
logDebug('finishReviewCoreLogic', `Updating note '${displayTitle(note)}' ...`)
- // First update @review(date) on the note
- updateMetadataInNote(note, [reviewedTodayString])
+ // If project metadata is in frontmatter, replace any body metadata line with migration message (or remove that message)
+ // before we recalculate the metadata line index and update mentions. This ensures that when both frontmatter and
+ // body metadata are present, we first migrate/merge them and then clean up @nextReview/@reviewed mentions once.
+ migrateProjectMetadataLineInNote(note)
+ const metadataLineIndex: number = getOrMakeMetadataLineIndex(note)
// Remove a @nextReview(date) if there is one, as that is used to skip a review, which is now done.
- deleteMetadataMentionInNote(note, [config.nextReviewMentionStr])
+ deleteMetadataMentionInNote(note, metadataLineIndex, [config.nextReviewMentionStr])
+ // Update @review(date) on the note
+ updateMetadataInNote(note, [reviewedTodayString])
// $FlowIgnore[prop-missing]
DataStore.updateCache(note, true)
}
@@ -926,11 +805,13 @@ async function finishReviewCoreLogic(note: CoreNoteFields): Promise {
// Save changes to allProjects list
await updateProjectInAllProjectsList(thisNoteAsProject)
- // Update display for user (but don't open if it isn't already)
- await renderProjectLists(config, false)
+ // Update display for user (if window is already open)
+ // TODO: How can we keep the scrollPos?
+ await renderProjectListsIfOpen(config)
} else {
// Regenerate whole list (and display if window is already open)
logInfo('finishReviewCoreLogic', `- In allProjects list couldn't find project '${note.filename}'. So regenerating whole list and will display if list is open.`)
+ // TODO: Split the following into just generate...(), and then move the renderProjectListsIfOpen() above to serve both if/else clauses
await generateProjectListsAndRenderIfOpen()
}
@@ -1179,9 +1060,9 @@ async function skipReviewCoreLogic(note: CoreNoteFields, skipIntervalOrDate: str
// Write changes to allProjects list
await updateProjectInAllProjectsList(thisNoteAsProject)
// Update display for user (but don't open window if not open already)
- await renderProjectLists(config, false)
+ await renderProjectListsIfOpen(config)
} else {
- // Regenerate whole list (and display if window is already open)
+ // Regenerate whole list (and display if window is already open)
logWarn('skipReviewCoreLogic', `- Couldn't find project '${note.filename}' in allProjects list. So regenerating whole list and display.`)
await generateProjectListsAndRenderIfOpen()
}
@@ -1322,7 +1203,7 @@ export async function setNewReviewInterval(noteArg?: TNote): Promise {
// Write changes to allProjects list
await updateProjectInAllProjectsList(thisNoteAsProject)
// Update display for user (but don't focus)
- await renderProjectLists(config, false)
+ await renderProjectListsIfOpen(config)
}
} catch (error) {
logError('setNewReviewInterval', error.message)
@@ -1353,7 +1234,8 @@ export async function toggleDisplayFinished(): Promise {
// logDebug('toggleDisplayFinished', `updatedConfig.displayFinished? now is '${String(updatedConfig.displayFinished)}'`)
const res = await DataStore.saveJSON(updatedConfig, '../jgclark.Reviews/settings.json', true)
// clo(updatedConfig, 'updatedConfig at end of toggle...()')
- await renderProjectLists(updatedConfig, false)
+ // TODO: how to get scrollPos?
+ await renderProjectListsIfOpen(updatedConfig)
}
catch (error) {
logError('toggleDisplayFinished', error.message)
@@ -1376,7 +1258,8 @@ export async function toggleDisplayOnlyDue(): Promise {
// logDebug('toggleDisplayOnlyDue', `updatedConfig.displayOnlyDue? now is '${String(updatedConfig.displayOnlyDue)}'`)
const res = await DataStore.saveJSON(updatedConfig, '../jgclark.Reviews/settings.json', true)
// clo(updatedConfig, 'updatedConfig at end of toggle...()')
- await renderProjectLists(updatedConfig, false)
+ // TODO: how to get scrollPos?
+ await renderProjectListsIfOpen(updatedConfig)
}
catch (error) {
logError('toggleDisplayOnlyDue', error.message)
@@ -1398,7 +1281,8 @@ export async function toggleDisplayNextActions(): Promise {
// logDebug('toggleDisplayNextActions', `updatedConfig.displayNextActions? now is '${String(updatedConfig.displayNextActions)}'`)
const res = await DataStore.saveJSON(updatedConfig, '../jgclark.Reviews/settings.json', true)
// clo(updatedConfig, 'updatedConfig at end of toggle...()')
- await renderProjectLists(updatedConfig, false)
+ // TODO: how to get scrollPos?
+ await renderProjectListsIfOpen(updatedConfig)
}
catch (error) {
logError('toggleDisplayNextActions', error.message)
@@ -1407,13 +1291,14 @@ export async function toggleDisplayNextActions(): Promise {
/**
* Save all display filter settings at once (used by Display filters dropdown).
- * @param {{ displayOnlyDue: boolean, displayFinished: boolean, displayPaused: boolean, displayNextActions: boolean }} data
+ * @param {{ displayOnlyDue: boolean, displayFinished: boolean, displayPaused: boolean, displayNextActions: boolean, displayOrder?: string }} data
*/
export async function saveDisplayFilters(data: {
displayOnlyDue: boolean,
displayFinished: boolean,
displayPaused: boolean,
displayNextActions: boolean,
+ displayOrder?: string,
}): Promise {
try {
const config: ReviewConfig = await getReviewSettings()
@@ -1421,8 +1306,11 @@ export async function saveDisplayFilters(data: {
config.displayFinished = data.displayFinished
config.displayPaused = data.displayPaused
config.displayNextActions = data.displayNextActions
+ if (typeof data.displayOrder === 'string' && data.displayOrder !== '') {
+ config.displayOrder = data.displayOrder
+ }
await DataStore.saveJSON(config, '../jgclark.Reviews/settings.json', true)
- await renderProjectLists(config, false)
+ await renderProjectListsIfOpen(config)
} catch (error) {
logError('saveDisplayFilters', error.message)
}
diff --git a/jgclark.Reviews/webfonts/fa-duotone-900.woff2 b/jgclark.Reviews/webfonts/fa-duotone-900.woff2
deleted file mode 100644
index 3f214a047..000000000
Binary files a/jgclark.Reviews/webfonts/fa-duotone-900.woff2 and /dev/null differ
diff --git a/jgclark.Reviews/webfonts/fa-regular-400.woff2 b/jgclark.Reviews/webfonts/fa-regular-400.woff2
deleted file mode 100644
index f08e2a2f5..000000000
Binary files a/jgclark.Reviews/webfonts/fa-regular-400.woff2 and /dev/null differ
diff --git a/jgclark.Reviews/webfonts/fa-solid-900.woff2 b/jgclark.Reviews/webfonts/fa-solid-900.woff2
deleted file mode 100644
index d75f8f7f4..000000000
Binary files a/jgclark.Reviews/webfonts/fa-solid-900.woff2 and /dev/null differ