Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat-dataview-source-multi-select #114

Merged
merged 2 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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