Skip to content

Commit

Permalink
Merge pull request #114 from danielo515/feat-dataview-source-multi-se…
Browse files Browse the repository at this point in the history
…lect

Feat-dataview-source-multi-select
  • Loading branch information
danielo515 authored Nov 3, 2023
2 parents 264df13 + 75f71f1 commit f9cd4e2
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 63 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"
}
28 changes: 20 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dependencies": {
"fp-ts": "^2.16.1",
"fuse.js": "^6.6.2",
"type-fest": "^4.6.0",
"valibot": "^0.19.0"
}
}
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
10 changes: 9 additions & 1 deletion src/core/formDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,20 @@ export type FormOptions = {
values?: Record<string, unknown>;
}

type KeyOfUnion<T> = T extends unknown ? keyof T : never
type PickUnion<T, K extends KeyOfUnion<T>> =
T extends unknown
? K & keyof T extends never ? never : Pick<T, K & keyof T>
: never

export type AllSources = PickUnion<inputType, 'source'>['source']

// When an input is in edit state, it is represented by this type.
// It has all the possible values, and then you need to narrow it down
// to the actual type.
export type EditableInput = {
type: AllFieldTypes;
source?: "notes" | "fixed";
source?: AllSources;
folder?: string;
min?: number;
max?: number;
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
11 changes: 6 additions & 5 deletions src/views/FormBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
log_error(
new ModalFormError(
"Unexpected error, no field at that index",
fieldIndex + " leads to undefined"
)
fieldIndex + " leads to undefined",
),
);
return Date.now() + "";
}
Expand All @@ -71,8 +71,8 @@
log_error(
new ModalFormError(
"Unexpected error, no field at that index",
fieldIndex + " leads to undefined"
)
fieldIndex + " leads to undefined",
),
);
return;
}
Expand Down Expand Up @@ -266,6 +266,7 @@
bind:source={field.input.source}
bind:options={field.input.multi_select_options}
bind:folder={field.input.folder}
bind:query={field.input.query}
notifyChange={onChange}
is_multi={true}
/>
Expand Down Expand Up @@ -327,7 +328,7 @@
id={delete_id}
on:click={() => {
definition.fields = definition.fields.filter(
(_, i) => i !== index
(_, i) => i !== index,
);
}}
/>
Expand Down
11 changes: 10 additions & 1 deletion src/views/components/InputBuilderSelect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import { setIcon } from "obsidian";
import FormRow from "./FormRow.svelte";
import InputFolder from "./InputFolder.svelte";
import { AllSources } from "src/core/formDefinition";
import InputBuilderDataview from "./inputBuilderDataview.svelte";
export let index: number;
export let source: string = "fixed";
export let source: AllSources = "fixed";
export let query: string = "";
export let folder: string | undefined;
export let options: option[] = [];
export let notifyChange: () => void;
Expand All @@ -35,6 +38,10 @@
<select bind:value={source} {id}>
<option value="fixed">Static</option>
<option value="notes">Notes</option>
{#if is_multi}
<!-- For now, only multi-select allows for dataview -->
<option value="dataview">Dataview</option>
{/if}
</select>
</FormRow>
{#if source === "fixed"}
Expand Down Expand Up @@ -125,6 +132,8 @@
</FormRow>
{:else if source === "notes"}
<InputFolder {index} bind:folder {notifyChange} />
{:else if source === "dataview"}
<InputBuilderDataview {index} bind:value={query} />
{/if}

<style>
Expand Down
Loading

0 comments on commit f9cd4e2

Please sign in to comment.