diff --git a/main.ts b/main.ts index 10165a0..8471f3f 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,5 @@ -import { Editor, MarkdownView, Notice, Plugin } from "obsidian"; +import { Notice, Plugin } from "obsidian"; import { getAPI } from "obsidian-dataview"; -import { addSimpleNoteReviewIcon } from "src/UI/icon"; import { NoteSetService } from "src/noteSet/noteSetService"; import { SelectNoteSetModal } from "src/UI/selectNoteSetModal"; import { @@ -71,9 +70,13 @@ export default class SimpleNoteReviewPlugin extends Plugin { new DefaultSettings(), await this.loadData() ); + + this.settings.noteSets = this.noteSetService.sortNoteSets(this.settings.noteSets); + await this.noteSetService.updateAllNotesetErrors(); } async saveSettings() { + this.settings.noteSets = this.noteSetService.sortNoteSets(this.settings.noteSets); await this.saveData(this.settings); } diff --git a/manifest.json b/manifest.json index 49dea05..6a068be 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "simple-note-review", "name": "Simple Note Review", - "version": "1.1.0", + "version": "1.2.0", "minAppVersion": "1.1.0", "description": "Simple, customizable plugin for easy note review, resurfacing & repetition.", "author": "dartungar", diff --git a/src/UI/noteset/noteSetEditModal.ts b/src/UI/noteset/noteSetEditModal.ts index 5510766..ae99603 100644 --- a/src/UI/noteset/noteSetEditModal.ts +++ b/src/UI/noteset/noteSetEditModal.ts @@ -1,5 +1,5 @@ import SimpleNoteReviewPlugin from "main"; -import { ButtonComponent, Modal, Setting, debounce, setIcon } from "obsidian"; +import { ButtonComponent, Modal, Setting } from "obsidian"; import { INoteSet } from "src/noteSet/INoteSet"; import { JoinLogicOperators } from "src/settings/joinLogicOperators"; @@ -157,7 +157,7 @@ export class NoteSetEditModal extends Modal { this._plugin.noteSetService.updateNoteSetStats(this._noteSet); await this._plugin.saveSettings(); await this._plugin.activateView(); - this._plugin.showNotice(`Note set "${this._noteSet.displayName}" saved.`); + this._plugin.showNotice(`Saved note set "${this._noteSet.displayName}".`); this.close(); } diff --git a/src/UI/settingsTab.ts b/src/UI/settingsTab.ts index 918835d..bbaea30 100644 --- a/src/UI/settingsTab.ts +++ b/src/UI/settingsTab.ts @@ -1,9 +1,7 @@ import SimpleNoteReviewPlugin from "main"; -import { App, PluginSettingTab, Setting, setIcon, debounce } from "obsidian"; -import { JoinLogicOperators } from "src/settings/joinLogicOperators"; +import { App, PluginSettingTab, Setting } from "obsidian"; import { NoteSetDeleteModal } from "src/UI/noteset/noteSetDeleteModal"; import { NoteSetInfoModal } from "src/UI/noteset/noteSetInfoModal"; -import { ReviewAlgorithm } from "../settings/reviewAlgorightms"; import { NoteSetEditModal } from "./noteset/noteSetEditModal"; export class SimpleNoteReviewPluginSettingsTab extends PluginSettingTab { @@ -15,7 +13,7 @@ export class SimpleNoteReviewPluginSettingsTab extends PluginSettingTab { this.display(); } - display(): void { + display(): void { const { containerEl } = this; containerEl.empty(); @@ -73,7 +71,7 @@ export class SimpleNoteReviewPluginSettingsTab extends PluginSettingTab { this._plugin.settings && this._plugin.settings.noteSets && - this._plugin.settings.noteSets.forEach((noteSet) => { + this._plugin.settings.noteSets.forEach((noteSet, index) => { this._plugin.noteSetService.updateNoteSetDisplayNameAndDescription( noteSet ); @@ -89,14 +87,10 @@ export class SimpleNoteReviewPluginSettingsTab extends PluginSettingTab { updateHeader(noteSet.displayName); - if (noteSet?.stats?.totalCount === 0) { + if (noteSet?.validationError) { setting.addExtraButton((cb) => { cb.setIcon("alert-triangle") - .setTooltip("this note set appears to be empty. if you're sure it's not, click this icon to refresh stats.") - .onClick(async () => { - await this._plugin.noteSetService.updateNoteSetStats(noteSet); - this.display(); - }); + .setTooltip(noteSet?.validationError); }); } @@ -112,11 +106,53 @@ export class SimpleNoteReviewPluginSettingsTab extends PluginSettingTab { }); }); + setting.addExtraButton((cb) => { + cb.setIcon("rotate-cw") + .setTooltip("Reset review queue and update stats for this note set") + .onClick(async () => { + await this._plugin.noteSetService.validateRules(noteSet); + await this._plugin.reviewService.resetNotesetQueue(noteSet); + await this._plugin.noteSetService.updateNoteSetStats(noteSet); + this.display(); + } + ); + }); + + setting.addExtraButton(cb => { + cb.setIcon('arrow-up') + .setTooltip("Move element up") + .setDisabled(index === 0) + .onClick(() => { + if (index > 0) { + const temp = this._plugin.settings.noteSets[index - 1].sortOrder; + this._plugin.settings.noteSets[index - 1].sortOrder = noteSet.sortOrder; + noteSet.sortOrder = temp; + this._plugin.saveSettings(); + this.display(); + } + }) + }); + + setting.addExtraButton(cb => { + cb.setIcon('arrow-down') + .setTooltip("Move element down") + .setDisabled(index >= this._plugin.settings.noteSets.length - 1) + .onClick(() => { + if (index < this._plugin.settings.noteSets.length - 1) { + const temp = this._plugin.settings.noteSets[index + 1].sortOrder; + this._plugin.settings.noteSets[index + 1].sortOrder = noteSet.sortOrder; + noteSet.sortOrder = temp; + this._plugin.saveSettings(); + this.display(); + } + }) + }); + setting.addExtraButton((cb) => { cb.setIcon("edit") .setTooltip("Edit Note set") .onClick(() => { - let modal = new NoteSetEditModal(noteSet, this._plugin); + const modal = new NoteSetEditModal(noteSet, this._plugin); modal.open(); modal.onClose = () => { this.refresh(); diff --git a/src/UI/sidebar/sidebarView.ts b/src/UI/sidebar/sidebarView.ts index 4d3370f..7af72e0 100644 --- a/src/UI/sidebar/sidebarView.ts +++ b/src/UI/sidebar/sidebarView.ts @@ -2,14 +2,11 @@ import SimpleNoteReviewPlugin from "main"; import { ItemView, Setting, - TAbstractFile, WorkspaceLeaf, - setIcon, } from "obsidian"; import { INoteSet } from "src/noteSet/INoteSet"; import { NoteSetInfoModal } from "../noteset/noteSetInfoModal"; import { ReviewFrequency } from "src/noteSet/reviewFrequency"; -import { NoteSetEditModal } from "../noteset/noteSetEditModal"; import { NoteSetEmptyError } from "src/noteSet/noteSetService"; export class SimpleNoteReviewSidebarView extends ItemView { @@ -29,7 +26,7 @@ export class SimpleNoteReviewSidebarView extends ItemView { // Nothing to clean up. } - async renderView(): Promise { + async renderView(): Promise { this.contentEl.empty(); this.createGeneralActionsEl(this.contentEl); @@ -44,7 +41,7 @@ export class SimpleNoteReviewSidebarView extends ItemView { } private createGeneralActionsEl(parentEl: HTMLElement): HTMLElement { - let actionsEl = new Setting(parentEl); + const actionsEl = new Setting(parentEl); actionsEl.setDesc("general actions:"); @@ -60,7 +57,9 @@ export class SimpleNoteReviewSidebarView extends ItemView { cb.setIcon("settings") .setTooltip("open plugin settings") .onClick(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (this.app as any).setting.open(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any (this.app as any).setting.openTabById("simple-note-review"); }); }); @@ -69,7 +68,7 @@ export class SimpleNoteReviewSidebarView extends ItemView { } private createCurrentFileActionsEl(parentEl: HTMLElement): HTMLElement { - let actionsEl = new Setting(parentEl); + const actionsEl = new Setting(parentEl); actionsEl.setDesc("current file actions:"); @@ -162,16 +161,10 @@ export class SimpleNoteReviewSidebarView extends ItemView { section.setDesc(""); } - if (noteSet?.stats?.totalCount === 0) { + if (noteSet?.validationError) { section.addExtraButton((cb) => { cb.setIcon("alert-triangle") - .setTooltip( - "this note set appears to be empty. if you're sure it's not, click this icon to refresh stats." - ) - .onClick(async () => { - await this._plugin.noteSetService.updateNoteSetStats(noteSet); - await this.renderView(); - }); + .setTooltip(noteSet?.validationError); }); } @@ -203,7 +196,7 @@ export class SimpleNoteReviewSidebarView extends ItemView { cb.setIcon("rotate-cw") .setTooltip("reset review queue for this note set") .onClick(async () => - this._plugin.reviewService.resetNotesetQueue(noteSet) + await this._plugin.reviewService.resetNotesetQueue(noteSet) ); }); diff --git a/src/dataview/dataviewFacade.ts b/src/dataview/dataviewFacade.ts index 8bcebb6..e5d654f 100644 --- a/src/dataview/dataviewFacade.ts +++ b/src/dataview/dataviewFacade.ts @@ -22,17 +22,18 @@ export class DataviewFacade { } public async pages(query: string): Promise>> { - // this.throwIfDataviewNotInstalled(); - // return this._api.pages(query); return await this.invokeAndReinitDvCacheOnError(() => this._api.pages(query)); } public async page(filepath: string): Promise> { - // this.throwIfDataviewNotInstalled(); - // return this._api.page(filepath); return await this.invokeAndReinitDvCacheOnError(() => this._api.page(filepath)); } + public async validate(query: string): Promise { + const result = await this.invokeAndReinitDvCacheOnError(() => this._api.query(`LIST FROM ${query}`)); + return result.successful; + } + public async getMetadataFieldValue(filepath: string, fieldName: string): Promise { const page = await this.page(filepath); return page[fieldName]; diff --git a/src/dataview/dataviewService.ts b/src/dataview/dataviewService.ts index b645c8c..9205640 100644 --- a/src/dataview/dataviewService.ts +++ b/src/dataview/dataviewService.ts @@ -55,6 +55,10 @@ export class DataviewService { return null; } + public validateQuery(query: string): Promise { + return this._dataviewApi.validate(query); + } + public getPageFromPath(filepath: string): Record { return this._dataviewApi.page(filepath); } diff --git a/src/noteSet/INoteSet.ts b/src/noteSet/INoteSet.ts index 8132f11..489b55b 100644 --- a/src/noteSet/INoteSet.ts +++ b/src/noteSet/INoteSet.ts @@ -5,6 +5,7 @@ import { INoteSetStats } from "./INoteSetStats" // TODO: excluded tags, folders, frontmatter keys export interface INoteSet { id: number + sortOrder: number | undefined name: string displayName: string description: string @@ -17,10 +18,12 @@ export interface INoteSet { dataviewQuery: string stats: INoteSetStats queue: INoteQueue + validationError: string | undefined } export class EmptyNoteSet implements INoteSet { id: number + sortOrder: undefined name: "new note set" displayName: string description: string @@ -33,6 +36,7 @@ export class EmptyNoteSet implements INoteSet { dataviewQuery: "" stats: INoteSetStats queue: INoteQueue + validationError: undefined constructor(id: number) { this.id = id; diff --git a/src/noteSet/noteSetInfoService.ts b/src/noteSet/noteSetInfoService.ts index 31ae06e..da0e07b 100644 --- a/src/noteSet/noteSetInfoService.ts +++ b/src/noteSet/noteSetInfoService.ts @@ -2,6 +2,7 @@ import { JoinLogicOperators } from "src/settings/joinLogicOperators"; import { INoteSet } from "./INoteSet"; import { DataviewService } from "../dataview/dataviewService"; import { getDateOffsetByNDays } from "src/utils/dateUtils"; +import { NoteSetService } from "./noteSetService"; export class NoteSetInfoService { @@ -33,10 +34,10 @@ export class NoteSetInfoService { private getNoteSetDescription(noteSet: INoteSet): string { if (this.queryMatchesAllNotes(noteSet)) { - return "matches all notes" + return NoteSetService.MATCHES_ALL_STRING; } - let desc: string[] = []; + const desc: string[] = []; if (noteSet.dataviewQuery && noteSet.dataviewQuery !== "") { desc.push(`are matched with dataviewJS query ${noteSet.dataviewQuery}; `); diff --git a/src/noteSet/noteSetService.ts b/src/noteSet/noteSetService.ts index 7a1d278..f208a9d 100644 --- a/src/noteSet/noteSetService.ts +++ b/src/noteSet/noteSetService.ts @@ -1,50 +1,114 @@ import { EmptyNoteSet, INoteSet } from "./INoteSet"; -import { App, TAbstractFile, TFile} from "obsidian"; +import { App } from "obsidian"; import SimpleNoteReviewPlugin from "main"; import { DataviewService } from "../dataview/dataviewService"; import { NoteSetInfoService } from "./noteSetInfoService"; - export class NoteSetEmptyError extends Error { - message = "Could not get the next note in note set. Please check note set settings and make sure it has notes."; + message = + "Could not get the next note in note set. Please check note set settings and make sure it has notes."; } export class OpenNextFileInNoteSetError extends Error { - message = "Could not open next note in note set. If this keeps happening, please try to disable and enable plugin. If that fails, try to restart Obsidian." + message = + "Could not open next note in note set. If this keeps happening, please try to disable and enable plugin. If that fails, try to restart Obsidian."; } -export class DataviewQueryError extends Error { } +export class DataviewQueryError extends Error {} export class NoteSetService { - private _dataviewService = new DataviewService(); - private _noteSetInfoService = new NoteSetInfoService(this._dataviewService); - - constructor(private _app: App, private _plugin: SimpleNoteReviewPlugin) { } - - public async addEmptyNoteSet() { - this._plugin.settings.noteSets.push( - new EmptyNoteSet( - this._plugin.settings.noteSets.length > 0 - ? Math.max(...this._plugin.settings.noteSets.map(q => q.id)) + 1 - : 1), // id "generation" - ); - await this._plugin.saveSettings(); - } - - public async deleteNoteSet(noteSet: INoteSet) { - this._plugin.settings.noteSets = this._plugin.settings.noteSets.filter(q => q.id !== noteSet.id); - await this._plugin.saveSettings(); - } - - public updateNoteSetDisplayNames() { - this._plugin.settings.noteSets.forEach(q => this.updateNoteSetDisplayNameAndDescription(q)); - } - - public updateNoteSetDisplayNameAndDescription(noteSet: INoteSet) { - this._noteSetInfoService.updateNoteSetDisplayNameAndDescription(noteSet); - } - - public async updateNoteSetStats(noteSet: INoteSet): Promise { - await this._noteSetInfoService.updateNoteSetStats(noteSet); - } - -} \ No newline at end of file + private _dataviewService = new DataviewService(); + private _noteSetInfoService = new NoteSetInfoService(this._dataviewService); + + public static readonly MATCHES_ALL_STRING = "matches all notes"; + + constructor(private _app: App, private _plugin: SimpleNoteReviewPlugin) {} + + public async addEmptyNoteSet() { + this._plugin.settings.noteSets.push( + new EmptyNoteSet( + this._plugin.settings.noteSets.length > 0 + ? Math.max( + ...this._plugin.settings.noteSets.map((q) => q.id)) + 1 + : 1 + ) // id "generation" + ); + await this._plugin.saveSettings(); + } + + public async deleteNoteSet(noteSet: INoteSet) { + this._plugin.settings.noteSets = this._plugin.settings.noteSets.filter( + (q) => q.id !== noteSet.id + ); + await this._plugin.saveSettings(); + } + + public updateNoteSetDisplayNames() { + this._plugin.settings.noteSets.forEach((q) => + this.updateNoteSetDisplayNameAndDescription(q) + ); + } + + public updateNoteSetDisplayNameAndDescription(noteSet: INoteSet) { + this._noteSetInfoService.updateNoteSetDisplayNameAndDescription( + noteSet + ); + } + + public async updateNoteSetStats(noteSet: INoteSet): Promise { + await this._noteSetInfoService.updateNoteSetStats(noteSet); + } + + public async updateAllNotesetErrors(): Promise { + await Promise.all(this._plugin.settings.noteSets.map(noteset => this.validateRules(noteset))); + } + + public async validateRules(noteSet: INoteSet): Promise { + this._plugin.settings.noteSets.find(x => x.id === noteSet.id).validationError = await this.notesetRuleError(noteSet); + await this._plugin.saveSettings(); + } + + private async notesetRuleError(noteset: INoteSet): Promise { + if (!noteset.queue?.filenames?.length) + return "noteset review queue is empty. if you are sure it should be not, try resetting queue and/or checking noteset rules."; + + const customDvQueryIsValid = !noteset.dataviewQuery || this._dataviewService.validateQuery(noteset.dataviewQuery); + if (!customDvQueryIsValid) + return "DataviewJS query is invalid"; + + const constructedDvQuery = this._dataviewService.getOrCreateBaseDataviewQuery(noteset); + const constructedDvQueryIsValid = await this._dataviewService.validateQuery(constructedDvQuery); + if (!constructedDvQueryIsValid) + return `noteset rules result in an invalid Dataview query: "${constructedDvQuery}".`; + + const queueActual = await this._dataviewService.getNoteSetFiles(noteset); + if (!queueActual.length) + return "noteset rules do not match any notes in the vault."; + + return undefined; + } + + public sortNoteSets(noteSets: INoteSet[]): INoteSet[] { + // Find the highest sortOrder that is defined + const maxSortOrder = noteSets.reduce((max, note) => { + if (note.sortOrder !== undefined && note.sortOrder > max) { + return note.sortOrder; + } + return max; + }, 0); + + // Fill undefined sortOrder values with incrementing numbers starting from maxSortOrder + 1 + let nextSortOrder = maxSortOrder + 1; + const filledNotes = noteSets.map((noteSet) => ({ + ...noteSet, + sortOrder: + noteSet.sortOrder !== undefined + ? noteSet.sortOrder + : nextSortOrder++, + })); + + // Now, sort the notes array by sortOrder + filledNotes.sort((a, b) => a.sortOrder - b.sortOrder); + + return filledNotes; + } +} diff --git a/src/queues/reviewService.ts b/src/queues/reviewService.ts index c2f68b0..2600b41 100644 --- a/src/queues/reviewService.ts +++ b/src/queues/reviewService.ts @@ -5,11 +5,9 @@ import { DataArray } from "obsidian-dataview"; import { INoteSet } from "src/noteSet/INoteSet"; import { calculateNoteReviewPriority } from "src/noteSet/noteReviewPriorityHelpers"; import { - OpenNextFileInNoteSetError, NoteSetEmptyError, } from "src/noteSet/noteSetService"; import { ReviewFrequency } from "src/noteSet/reviewFrequency"; -import { ReviewAlgorithm } from "src/settings/reviewAlgorightms"; import { DataviewService } from "src/dataview/dataviewService"; export class ReviewService { @@ -54,8 +52,8 @@ export class ReviewService { public async openRandomNoteInQueue(noteSet: INoteSet) { await this.createNotesetQueueIfNotExists(noteSet); - let randomIndex = Math.floor(Math.random() * noteSet.queue.filenames.length); - let filePath = noteSet.queue.filenames[randomIndex]; + const randomIndex = Math.floor(Math.random() * noteSet.queue.filenames.length); + const filePath = noteSet.queue.filenames[randomIndex]; const abstractFile = this._app.vault.getAbstractFileByPath(filePath); await this._app.workspace.getMostRecentLeaf().openFile(abstractFile as TFile); } @@ -72,13 +70,17 @@ export class ReviewService { } private async openNextNoteInQueue(noteSet: INoteSet): Promise { - let filePath = noteSet.queue.filenames[0]; + const filePath = noteSet.queue.filenames[0]; const abstractFile = this._app.vault.getAbstractFileByPath(filePath); await this._app.workspace.getMostRecentLeaf().openFile(abstractFile as TFile); } private async createNotesetQueue(noteSet: INoteSet): Promise { - let files = await this.generateNotesetQueue(noteSet); + if (noteSet.validationError) { + this._plugin.showNotice(`note set "${noteSet.displayName}" has validation errors; you might want to fix them before starting the review.`) + return; + } + const files = await this.generateNotesetQueue(noteSet); noteSet.queue = new NoteQueue(files); await this._plugin.saveSettings(); }