Skip to content

Commit

Permalink
feat(inputs): dataview can be used as source for multi-select
Browse files Browse the repository at this point in the history
fixes #50
  • Loading branch information
danielo515 committed Nov 3, 2023
1 parent 264df13 commit cd39785
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 25 deletions.
6 changes: 5 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"plugins": [
"prettier-plugin-svelte"
]
],
"arrowParens": "always",
"editorconfig": true,
"svelteAllowShorthand": true,
"trailingComma": "all"
}
8 changes: 6 additions & 2 deletions src/FormModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { FormDefinition, FormOptions } from "./core/formDefinition";
import { FileSuggest } from "./suggesters/suggestFile";
import { DataviewSuggest } from "./suggesters/suggestFromDataview";
import { SvelteComponent } from "svelte";
import { executeSandboxedDvQuery, sandboxedDvQuery } from "./suggesters/SafeDataviewQuery";

export type SubmitFn = (formResult: FormResult) => void;

Expand Down Expand Up @@ -131,9 +132,12 @@ export class FormModal extends Modal {
case 'multiselect':
{
this.formResult[definition.name] = this.formResult[definition.name] || []
const options = fieldInput.source == 'fixed'
const source = fieldInput.source;
const options = source == 'fixed'
? fieldInput.multi_select_options
: get_tfiles_from_folder(fieldInput.folder, this.app).map((file) => file.basename);
: source == 'notes'
? get_tfiles_from_folder(fieldInput.folder, this.app).map((file) => file.basename)
: executeSandboxedDvQuery(sandboxedDvQuery(fieldInput.query), this.app)
this.svelteComponents.push(new MultiSelect({
target: fieldBase.controlEl,
props: {
Expand Down
7 changes: 6 additions & 1 deletion src/core/formDefinitionSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ const MultiSelectNotesSchema = object({
folder: nonEmptyString('multi select source folder')
});
const MultiSelectFixedSchema = object({ type: literal("multiselect"), source: literal("fixed"), multi_select_options: array(string()) });
export const MultiselectSchema = union([MultiSelectNotesSchema, MultiSelectFixedSchema]);
const MultiSelectQuerySchema = object({
type: literal("multiselect"),
source: literal("dataview"),
query: nonEmptyString('dataview query')
});
export const MultiselectSchema = union([MultiSelectNotesSchema, MultiSelectFixedSchema, MultiSelectQuerySchema]);
export const InputTypeSchema = union([
InputBasicSchema,
InputNoteFromFolderSchema,
Expand Down
6 changes: 6 additions & 0 deletions src/exampleModalDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export const exampleModalDefinition: FormDefinition = {
description: "Allows to pick many notes from a fixed list",
input: { type: "multiselect", source: "fixed", multi_select_options: ['Android', 'iOS', 'Windows', 'MacOS', 'Linux', 'Solaris', 'MS2'] },
},
{
name: "multi_select_dataview",
label: "Multi select dataview",
description: "Allows to pick several values from a dv query",
input: { type: "multiselect", source: "dataview", query: 'dv.pages("#person").map(p => p.file.name)' },
},
{
name: "best_fried",
label: "Best friend",
Expand Down
9 changes: 6 additions & 3 deletions src/std/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { pipe as p } from "fp-ts/function";
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 } from "fp-ts/Either";
import { isLeft, isRight, tryCatchK, map, getOrElse, 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";
export type { NonEmptyArray } from 'fp-ts/NonEmptyArray'

export type { Either, Left, Right } from 'fp-ts/Either'
export const flow = f;
export const pipe = p
export const A = {
partitionMap,
Expand All @@ -23,10 +24,12 @@ export const E = {
left,
right,
tryCatchK,
tryCatch,
getOrElse,
map,
mapLeft,
bimap,
flatMap,
}

export const O = {
Expand Down
56 changes: 56 additions & 0 deletions src/suggesters/SafeDataviewQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { E, Either, flow } from "@std";
import { pipe } from "fp-ts/lib/function";
import { App } from "obsidian";
import { ModalFormError } from "src/utils/Error";
import { log_error } from "src/utils/Log";

type DataviewQuery = (dv: unknown, pages: unknown) => unknown;
export type SafeDataviewQuery = (dv: unknown, pages: unknown) => Either<ModalFormError, string[]>;
/**
* From a string representing a dataview query, it returns the safest possible
* function that can be used to evaluate the query.
* The function is sandboxed and it will return an Either<ModalFormError, string[]>.
* If you want a convenient way to execute the query, use executeSandboxedDvQuery.
* @param query string representing a dataview query that will be evaluated in a sandboxed environment
* @returns SafeDataviewQuery
*/
export function sandboxedDvQuery(query: string): SafeDataviewQuery {
if (!query.startsWith('return')) {
query = 'return ' + query;
}
const run = new Function('dv', 'pages', query) as DataviewQuery;
return flow(
E.tryCatchK(run, () => new ModalFormError('Error evaluating the dataview query')),
E.flatMap((result) => {
if (!Array.isArray(result)) {
return E.left(new ModalFormError('The dataview query did not return an array'));
}
return E.right(result);
})
);
}

/**
* Executes and unwraps the result of a SafeDataviewQuery.
* Use this function if you want a convenient way to execute the query.
* It will log the errors to the UI and return an empty array if the query fails.
* @param query SafeDataviewQuery to execute
* @param app the global obsidian app
* @returns string[] if the query was executed successfully, otherwise an empty array
*/
export function executeSandboxedDvQuery(query: SafeDataviewQuery, app: App): string[] {
const dv = app.plugins.plugins.dataview?.api;

if (!dv) {
log_error(new ModalFormError("Dataview plugin is not enabled"))
return [] as string[];
}
const pages = dv.pages;
return pipe(
query(dv, pages),
E.getOrElse((e) => {
log_error(e);
return [] as string[];
})
)
}
23 changes: 5 additions & 18 deletions src/suggesters/suggestFromDataview.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,26 @@
import { AbstractInputSuggest, App } from "obsidian";
import { ModalFormError, tryCatch } from "src/utils/Error";
import { log_error } from "src/utils/Log";
import { SafeDataviewQuery, executeSandboxedDvQuery, sandboxedDvQuery } from "./SafeDataviewQuery";

/**
* Offers suggestions based on a dataview query.
* It requires the dataview plugin to be installed and enabled.
* For now, we are not very strict with the checks and just throw errors
*/
export class DataviewSuggest extends AbstractInputSuggest<string> {
sandboxedQuery: (dv: any, pages: any) => string[]
sandboxedQuery: SafeDataviewQuery

constructor(
public inputEl: HTMLInputElement,
dvQuery: string,
public app: App,
) {
super(app, inputEl);
this.sandboxedQuery = tryCatch(
() => eval(`(function sandboxedQuery(dv, pages) { return ${dvQuery} })`),
"Invalid dataview query"
)
this.sandboxedQuery = sandboxedDvQuery(dvQuery)
}

getSuggestions(inputStr: string): string[] {
const dv = this.app.plugins.plugins.dataview?.api
if (!dv) {
log_error(new ModalFormError("Dataview plugin is not enabled"))
return [];
}
const result = this.sandboxedQuery(dv, dv.pages)
if (!Array.isArray(result)) {
log_error(new ModalFormError("The dataview query did not return an array"))
return [];
}
return result.filter(r => r.toLowerCase().includes(inputStr.toLowerCase()))
const result = executeSandboxedDvQuery(this.sandboxedQuery, this.app)
return result.filter((r) => r.toLowerCase().includes(inputStr.toLowerCase()))
}

renderSuggestion(option: string, el: HTMLElement): void {
Expand Down

0 comments on commit cd39785

Please sign in to comment.