From 0d5afbd399a6635c77534c3fb0e9b2838ee22040 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Sat, 4 Nov 2023 00:52:37 +0100 Subject: [PATCH 1/3] fix(input): if folder does not exist, the form does not fail fixes #90 --- src/FormModal.ts | 43 +++++++++++---- src/core/formDefinition.ts | 2 + src/std/index.ts | 3 +- src/suggesters/suggestFile.ts | 12 ++-- src/utils/files.ts | 101 ++++++++++++++++++++++------------ 5 files changed, 105 insertions(+), 56 deletions(-) diff --git a/src/FormModal.ts b/src/FormModal.ts index 71e6e41e..0c41f425 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -8,6 +8,9 @@ import { FileSuggest } from "./suggesters/suggestFile"; import { DataviewSuggest } from "./suggesters/suggestFromDataview"; import { SvelteComponent } from "svelte"; import { executeSandboxedDvQuery, sandboxedDvQuery } from "./suggesters/SafeDataviewQuery"; +import { pipe } from "fp-ts/lib/function"; +import { A, E } from "@std"; +import { log_error } from "./utils/Log"; export type SubmitFn = (formResult: FormResult) => void; @@ -136,7 +139,14 @@ export class FormModal extends Modal { const options = source == 'fixed' ? fieldInput.multi_select_options : source == 'notes' - ? get_tfiles_from_folder(fieldInput.folder, this.app).map((file) => file.basename) + ? pipe( + get_tfiles_from_folder(fieldInput.folder, this.app), + E.map(A.map((file) => file.basename)), + E.getOrElse((err) => { + log_error(err) + return [] as string[]; + }) + ) : executeSandboxedDvQuery(sandboxedDvQuery(fieldInput.query), this.app) this.svelteComponents.push(new MultiSelect({ target: fieldBase.controlEl, @@ -182,18 +192,27 @@ export class FormModal extends Modal { case "notes": return fieldBase.addDropdown((element) => { const files = get_tfiles_from_folder(fieldInput.folder, this.app); - const options = files.reduce( - ( - acc: Record, - option - ) => { - acc[option.basename] = - option.basename; - return acc; - }, - {} + pipe( + files, + E.map((files) => files.reduce( + ( + acc: Record, + option + ) => { + acc[option.basename] = + option.basename; + return acc; + }, + {} + )), + E.mapLeft((err) => { + log_error(err); + return err; + }), + E.map((options) => { + element.addOptions(options) + }) ); - element.addOptions(options); this.formResult[definition.name] = element.getValue(); element.onChange(async (value) => { this.formResult[definition.name] = diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts index 00adefca..9ed0aa79 100644 --- a/src/core/formDefinition.ts +++ b/src/core/formDefinition.ts @@ -86,6 +86,8 @@ export type EditableFormDefinition = { label?: string; description: string; input: EditableInput; + folder?: string; + options?: { value: string; label: string }[]; }[]; }; diff --git a/src/std/index.ts b/src/std/index.ts index c04cbb78..6a576241 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -1,7 +1,7 @@ import { pipe as p, flow as f } from "fp-ts/function"; import { partitionMap, findFirst, findFirstMap, partition, map as mapArr, filter } from "fp-ts/Array"; import { map as mapO, getOrElse as getOrElseOpt, some, none } from 'fp-ts/Option' -import { isLeft, isRight, tryCatchK, map, getOrElse, right, left, mapLeft, Either, bimap, tryCatch, flatMap } from "fp-ts/Either"; +import { isLeft, isRight, tryCatchK, map, getOrElse, fromNullable, right, left, mapLeft, Either, bimap, tryCatch, flatMap } from "fp-ts/Either"; import { BaseSchema, Output, ValiError, parse as parseV } from "valibot"; import { Semigroup, concatAll } from "fp-ts/Semigroup"; import { NonEmptyArray } from "fp-ts/NonEmptyArray"; @@ -30,6 +30,7 @@ export const E = { mapLeft, bimap, flatMap, + fromNullable, } export const O = { diff --git a/src/suggesters/suggestFile.ts b/src/suggesters/suggestFile.ts index 36cfc498..19a87e31 100644 --- a/src/suggesters/suggestFile.ts +++ b/src/suggesters/suggestFile.ts @@ -1,6 +1,6 @@ import { AbstractInputSuggest, App, TAbstractFile, TFile } from "obsidian"; import { get_tfiles_from_folder } from "../utils/files"; -import { tryCatch } from "../utils/Error"; +import { E } from "@std"; // Instead of hardcoding the logic in separate and almost identical classes, // we move this little logic parts into an interface and we can use the samme @@ -10,6 +10,7 @@ export interface FileStrategy { selectSuggestion(file: TFile): string; } + export class FileSuggest extends AbstractInputSuggest { constructor( public app: App, @@ -21,17 +22,14 @@ export class FileSuggest extends AbstractInputSuggest { } getSuggestions(input_str: string): TFile[] { - const all_files = tryCatch( - () => get_tfiles_from_folder(this.folder, this.app), - "The folder does not exist" - ); - if (!all_files) { + const all_files = get_tfiles_from_folder(this.folder, this.app) + if (E.isLeft(all_files)) { return []; } const lower_input_str = input_str.toLowerCase(); - return all_files.filter((file: TAbstractFile) => { + return all_files.right.filter((file: TAbstractFile) => { return ( file instanceof TFile && file.extension === "md" && diff --git a/src/utils/files.ts b/src/utils/files.ts index 48cd5673..1632d97b 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -1,47 +1,76 @@ import { App, TAbstractFile, TFile, TFolder, Vault, normalizePath } from "obsidian"; -import { ModalFormError } from "./Error"; - -export function resolve_tfolder(folder_str: string, app: App): TFolder { - folder_str = normalizePath(folder_str); +import { E, Either, pipe } from "@std"; +export class FolderDoesNotExistError extends Error { + static readonly tag = "FolderDoesNotExistError"; +} - const folder = app.vault.getAbstractFileByPath(folder_str); - if (!folder) { - throw new ModalFormError(`Folder "${folder_str}" doesn't exist`); - } - if (!(folder instanceof TFolder)) { - throw new ModalFormError(`${folder_str} is a file, not a folder`); - } +export class NotAFolderError extends Error { + static readonly tag = "NotAFolderError"; + constructor(public file: TAbstractFile) { + super(`File ${file.path} is not a folder`); + } +} - return folder; +export class FileDoesNotExistError extends Error { + static readonly tag = "FileDoesNotExistError"; + static of(file: string) { + return new FileDoesNotExistError(`File "${file}" doesn't exist`); + } +} +export class NotAFileError extends Error { + static readonly tag = "NotAFileError"; + constructor(public file: TAbstractFile) { + super(`File ${file.path} is not a file`); + } } -export function resolve_tfile(file_str: string, app: App): TFile { - file_str = normalizePath(file_str); +type FolderError = FolderDoesNotExistError | NotAFolderError; - const file = app.vault.getAbstractFileByPath(file_str); - if (!file) { - throw new ModalFormError(`File "${file_str}" doesn't exist`); - } - if (!(file instanceof TFile)) { - throw new ModalFormError(`${file_str} is a folder, not a file`); - } +export function resolve_tfolder(folder_str: string, app: App): Either { + folder_str = normalizePath(folder_str); - return file; + return pipe( + app.vault.getAbstractFileByPath(folder_str), + E.fromNullable(new FolderDoesNotExistError(`Folder "${folder_str}" doesn't exist`)), + E.flatMap((file) => { + if (!(file instanceof TFolder)) { + return E.left(new NotAFolderError(file)); + } + return E.right(file); + }) + ); } -export function get_tfiles_from_folder(folder_str: string, app: App): Array { - const folder = resolve_tfolder(folder_str, app); - - const files: Array = []; - Vault.recurseChildren(folder, (file: TAbstractFile) => { - if (file instanceof TFile) { - files.push(file); - } - }); - - files.sort((a, b) => { - return a.basename.localeCompare(b.basename); - }); +export function resolve_tfile(file_str: string, app: App): Either { + return pipe( + normalizePath(file_str), + app.vault.getAbstractFileByPath, + E.fromNullable(FileDoesNotExistError.of(file_str)), + E.flatMap((file) => { + if (!(file instanceof TFile)) { + return E.left(new NotAFileError(file)); + } + return E.right(file); + }) + ) +} - return files; +export function get_tfiles_from_folder(folder_str: string, app: App): Either> { + return pipe( + resolve_tfolder(folder_str, app), + E.flatMap((folder) => { + const files: Array = []; + Vault.recurseChildren(folder, (file: TAbstractFile) => { + if (file instanceof TFile) { + files.push(file); + } + }); + return E.right(files); + }), + E.map((files) => { + return files.sort((a, b) => { + return a.basename.localeCompare(b.basename); + }); + } + )) } From a89d1a259dcadb2f12cfb800f80e25047ab2c011 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Sat, 4 Nov 2023 00:59:19 +0100 Subject: [PATCH 2/3] chore: remove floating vars --- src/utils/files.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/utils/files.ts b/src/utils/files.ts index 1632d97b..4eed9b2f 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -27,10 +27,9 @@ export class NotAFileError extends Error { type FolderError = FolderDoesNotExistError | NotAFolderError; export function resolve_tfolder(folder_str: string, app: App): Either { - folder_str = normalizePath(folder_str); - return pipe( - app.vault.getAbstractFileByPath(folder_str), + normalizePath(folder_str), + (path) => app.vault.getAbstractFileByPath(path), E.fromNullable(new FolderDoesNotExistError(`Folder "${folder_str}" doesn't exist`)), E.flatMap((file) => { if (!(file instanceof TFolder)) { @@ -44,7 +43,7 @@ export function resolve_tfolder(folder_str: string, app: App): Either { return pipe( normalizePath(file_str), - app.vault.getAbstractFileByPath, + (path) => app.vault.getAbstractFileByPath(path), E.fromNullable(FileDoesNotExistError.of(file_str)), E.flatMap((file) => { if (!(file instanceof TFile)) { From 8567b8aa98b190b810038f8293f0bb51ca2f8792 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Sat, 4 Nov 2023 01:04:07 +0100 Subject: [PATCH 3/3] chore: remove unused code --- src/API.ts | 2 +- src/core/FormResult.ts | 2 +- src/main.ts | 2 +- src/suggesters/SafeDataviewQuery.ts | 2 +- src/utils/Error.ts | 45 ----------------------------- src/utils/Log.ts | 2 +- src/utils/ModalFormError.ts | 7 +++++ src/views/FormBuilder.svelte | 2 +- 8 files changed, 13 insertions(+), 51 deletions(-) delete mode 100644 src/utils/Error.ts create mode 100644 src/utils/ModalFormError.ts diff --git a/src/API.ts b/src/API.ts index 72fc329f..d047484e 100644 --- a/src/API.ts +++ b/src/API.ts @@ -5,7 +5,7 @@ import { MigrationError } from "./core/formDefinitionSchema"; import FormResult from "./core/FormResult"; import { exampleModalDefinition } from "./exampleModalDefinition"; import ModalFormPlugin from "./main"; -import { ModalFormError } from "./utils/Error"; +import { ModalFormError } from "./utils/ModalFormError"; import { FormModal } from "./FormModal"; import { log_error, log_notice } from "./utils/Log"; diff --git a/src/core/FormResult.ts b/src/core/FormResult.ts index 7cb4d54b..9c08c233 100644 --- a/src/core/FormResult.ts +++ b/src/core/FormResult.ts @@ -1,7 +1,7 @@ import { objectSelect } from './objectSelect'; import { stringifyYaml } from "obsidian"; import { log_error } from "../utils/Log"; -import { ModalFormError } from "../utils/Error"; +import { ModalFormError } from "../utils/ModalFormError"; type ResultStatus = "ok" | "cancelled"; diff --git a/src/main.ts b/src/main.ts index f0b2f9dc..cea8baa7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,7 @@ import { ModalFormSettingTab } from "src/ModalFormSettingTab"; import { API } from "src/API"; import { EDIT_FORM_VIEW, EditFormView } from "src/views/EditFormView"; import { MANAGE_FORMS_VIEW, ManageFormsView } from "src/views/ManageFormsView"; -import { ModalFormError } from "src/utils/Error"; +import { ModalFormError } from "src/utils/ModalFormError"; import { type FormDefinition } from "src/core/formDefinition"; import { formNeedsMigration, migrateToLatest, MigrationError, InvalidData } from "./core/formDefinitionSchema"; import { parseSettings, type ModalFormSettings, type OpenPosition, getDefaultSettings } from "src/core/settings"; diff --git a/src/suggesters/SafeDataviewQuery.ts b/src/suggesters/SafeDataviewQuery.ts index 220a860b..3ed5ff2e 100644 --- a/src/suggesters/SafeDataviewQuery.ts +++ b/src/suggesters/SafeDataviewQuery.ts @@ -1,7 +1,7 @@ import { E, Either, flow } from "@std"; import { pipe } from "fp-ts/lib/function"; import { App } from "obsidian"; -import { ModalFormError } from "src/utils/Error"; +import { ModalFormError } from "src/utils/ModalFormError"; import { log_error } from "src/utils/Log"; type DataviewQuery = (dv: unknown, pages: unknown) => unknown; diff --git a/src/utils/Error.ts b/src/utils/Error.ts deleted file mode 100644 index e98b84d8..00000000 --- a/src/utils/Error.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { log_error } from "./Log"; - -export class ModalFormError extends Error { - constructor(msg: string, public console_msg?: string) { - super(msg); - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} - -export async function errorWrapper( - fn: () => Promise, - msg: string -): Promise { - try { - return await fn(); - } catch (e) { - const err = e instanceof Error ? e : new Error(String(e)); - if (!(err instanceof ModalFormError)) { - log_error(new ModalFormError(msg, err.message)); - } else { - log_error(err); - } - return null; - } -} - -/** - * I case of error, logs it to the console and to the UI - * and returns null - * @export - * @template T - * @param {() => T} fn - * @param {string} msg - * @return {*} {(T | null)} - */ -export function tryCatch(fn: () => T, msg: string): T | null { - try { - return fn(); - } catch (e) { - if (e instanceof Error) - log_error(new ModalFormError(msg, e.message)); - return null; - } -} diff --git a/src/utils/Log.ts b/src/utils/Log.ts index 6396aeca..b4e4b876 100644 --- a/src/utils/Log.ts +++ b/src/utils/Log.ts @@ -1,5 +1,5 @@ import { Notice } from "obsidian"; -import { ModalFormError } from "./Error"; +import { ModalFormError } from "./ModalFormError"; export function log_notice(title: string, msg: string | DocumentFragment): void { diff --git a/src/utils/ModalFormError.ts b/src/utils/ModalFormError.ts new file mode 100644 index 00000000..70112152 --- /dev/null +++ b/src/utils/ModalFormError.ts @@ -0,0 +1,7 @@ +export class ModalFormError extends Error { + constructor(msg: string, public console_msg?: string) { + super(msg); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/src/views/FormBuilder.svelte b/src/views/FormBuilder.svelte index 5bd1f603..f54bf4d5 100644 --- a/src/views/FormBuilder.svelte +++ b/src/views/FormBuilder.svelte @@ -11,7 +11,7 @@ import InputBuilderSelect from "./components/InputBuilderSelect.svelte"; import InputFolder from "./components/InputFolder.svelte"; import { log_error } from "src/utils/Log"; - import { ModalFormError } from "src/utils/Error"; + import { ModalFormError } from "src/utils/ModalFormError"; export let definition: EditableFormDefinition = { title: "",