Skip to content

Commit

Permalink
Merge pull request #195 from danielo515/feat/allow-unknown-values-in-…
Browse files Browse the repository at this point in the history
…multiselect

feat: tags input allow any value to be selected, even if it does not exist yet
  • Loading branch information
danielo515 authored Jan 2, 2024
2 parents 0e7e24b + b8d0826 commit 5cfe8ff
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 140 deletions.
28 changes: 22 additions & 6 deletions src/FormModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import { log_error, log_notice } from "./utils/Log";
import { FieldValue, FormEngine, makeFormEngine } from "./store/formStore";
import { Writable } from "svelte/store";
import { FolderSuggest } from "./suggesters/suggestFolder";
import { allowsUnknownValues } from "./core/InputDefinitionSchema";

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

const notify = throttle((msg: string) => log_notice("⚠️ The form has errors ⚠️", msg, "notice-warning"), 2000);
const notify = throttle(
(msg: string) => log_notice("⚠️ The form has errors ⚠️", msg, "notice-warning"),
2000,
);
const notifyError = (title: string) =>
throttle((msg: string) => log_notice(`🚨 ${title} 🚨`, msg, "notice-error"), 2000);

Expand All @@ -34,7 +38,10 @@ export class FormModal extends Modal {
options?: FormOptions,
) {
super(app);
this.initialFormValues = formDataFromFormDefaults(modalDefinition.fields, options?.values ?? {});
this.initialFormValues = formDataFromFormDefaults(
modalDefinition.fields,
options?.values ?? {},
);
this.formEngine = makeFormEngine((result) => {
this.onSubmit(FormResult.make(result, "ok"));
this.close();
Expand All @@ -46,7 +53,8 @@ export class FormModal extends Modal {
const { contentEl } = this;
// This class is very important for scoped styles
contentEl.addClass("modal-form");
if (this.modalDefinition.customClassname) contentEl.addClass(this.modalDefinition.customClassname);
if (this.modalDefinition.customClassname)
contentEl.addClass(this.modalDefinition.customClassname);
contentEl.createEl("h1", { text: this.modalDefinition.title });
this.modalDefinition.fields.forEach((definition) => {
const fieldBase = new Setting(contentEl)
Expand Down Expand Up @@ -155,19 +163,23 @@ export class FormModal extends Modal {
});
case "multiselect": {
const source = fieldInput.source;
const allowUnknownValues = allowsUnknownValues(fieldInput);
const options =
source == "fixed"
? fieldInput.multi_select_options
: source == "notes"
? pipe(
? 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);
: executeSandboxedDvQuery(
sandboxedDvQuery(fieldInput.query),
this.app,
);
fieldStore.value.set(initialValue ?? []);
this.svelteComponents.push(
new MultiSelect({
Expand All @@ -178,13 +190,16 @@ export class FormModal extends Modal {
errors: fieldStore.errors,
setting: fieldBase,
app: this.app,
allowUnknownValues,
},
}),
);
return;
}
case "tag": {
const options = Object.keys(this.app.metadataCache.getTags()).map((tag) => tag.slice(1)); // remove the #
const options = Object.keys(this.app.metadataCache.getTags()).map((tag) =>
tag.slice(1),
); // remove the #
fieldStore.value.set(initialValue ?? []);
this.svelteComponents.push(
new MultiSelect({
Expand All @@ -195,6 +210,7 @@ export class FormModal extends Modal {
setting: fieldBase,
errors: fieldStore.errors,
app: this.app,
allowUnknownValues: true,
},
}),
);
Expand Down
23 changes: 18 additions & 5 deletions src/core/InputDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { trySchemas, ParsingFn, parseC } from "@std";
import { AllFieldTypes } from "./formDefinition";
import { AllFieldTypes, AllSources } from "./formDefinition";
import {
object,
number,
Expand All @@ -13,6 +13,7 @@ import {
BaseSchema,
enumType,
Output,
boolean,
} from "valibot";

/**
Expand Down Expand Up @@ -87,12 +88,27 @@ const MultiSelectFixedSchema = object({
type: literal("multiselect"),
source: literal("fixed"),
multi_select_options: array(string()),
allowUnknownValues: optional(boolean(), false),
});
const MultiSelectQuerySchema = object({
type: literal("multiselect"),
source: literal("dataview"),
query: nonEmptyString("dataview query"),
allowUnknownValues: optional(boolean(), false),
});

export function canAllowUnknownValues(
type: "multiselect",
source: AllSources,
): source is "dataview" | "fixed" {
return type === "multiselect" && (source === "dataview" || source === "fixed");
}

export function allowsUnknownValues(input: multiselect): boolean {
if (input.source === "notes") return false;
return input.allowUnknownValues;
}

export const MultiselectSchema = union([
MultiSelectNotesSchema,
MultiSelectFixedSchema,
Expand Down Expand Up @@ -120,10 +136,7 @@ export const InputTypeSchema = union([
DocumentBlock,
]);

export const InputTypeToParserMap: Record<
AllFieldTypes,
ParsingFn<BaseSchema>
> = {
export const InputTypeToParserMap: Record<AllFieldTypes, ParsingFn<BaseSchema>> = {
number: parseC(InputBasicSchema),
text: parseC(InputBasicSchema),
email: parseC(InputBasicSchema),
Expand Down
3 changes: 2 additions & 1 deletion src/core/formDataFromDefaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe("formDataFromFormOptions", () => {
input: {
type: "multiselect",
source: "fixed",
allowUnknownValues: false,
multi_select_options: ["reading", "swimming", "running"],
},
},
Expand Down Expand Up @@ -61,7 +62,7 @@ describe("formDataFromFormOptions", () => {
expect(result).toEqual(expectedFormData);
});

it("should ensure toggles always have avalue even when no values are provided", () => {
it("should ensure toggles always have a value even when no values are provided", () => {
const values = {};

const expectedFormData = { isEmployed: false };
Expand Down
45 changes: 14 additions & 31 deletions src/core/formDefinition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,25 @@ import { parse } from "valibot";

describe("isDataViewSource", () => {
it("should return true for valid inputDataviewSource objects", () => {
expect(
isDataViewSource({ type: "dataview", query: "some query" }),
).toBe(true);
expect(isDataViewSource({ type: "dataview", query: "some query" })).toBe(true);
});

it("should return false for invalid inputDataviewSource objects", () => {
expect(isDataViewSource({ type: "dataview" })).toBe(false);
expect(isDataViewSource({ type: "dataview", query: 123 })).toBe(false);
expect(isDataViewSource({ type: "select", query: "some query" })).toBe(
false,
);
expect(isDataViewSource({ type: "select", query: "some query" })).toBe(false);
});
});

describe("isInputNoteFromFolder", () => {
it("should return true for valid inputNoteFromFolder objects", () => {
expect(
isInputNoteFromFolder({ type: "note", folder: "some folder" }),
).toBe(true);
expect(isInputNoteFromFolder({ type: "note", folder: "some folder" })).toBe(true);
});

it("should return false for invalid inputNoteFromFolder objects", () => {
expect(isInputNoteFromFolder({ type: "note" })).toBe(false);
expect(isInputNoteFromFolder({ type: "note", folder: 123 })).toBe(
false,
);
expect(
isInputNoteFromFolder({ type: "select", folder: "some folder" }),
).toBe(false);
expect(isInputNoteFromFolder({ type: "note", folder: 123 })).toBe(false);
expect(isInputNoteFromFolder({ type: "select", folder: "some folder" })).toBe(false);
});
});

Expand All @@ -54,9 +44,7 @@ describe("isInputSelectFixed", () => {
});

it("should return false for invalid inputSelectFixed objects", () => {
expect(isInputSelectFixed({ type: "select", source: "fixed" })).toBe(
false,
);
expect(isInputSelectFixed({ type: "select", source: "fixed" })).toBe(false);
expect(
isInputSelectFixed({
type: "select",
Expand All @@ -81,9 +69,7 @@ describe("isInputSlider", () => {

it("should return false for invalid inputSlider objects", () => {
expect(isInputSlider({ type: "slider" })).toBe(false);
expect(isInputSlider({ type: "slider", min: "0", max: 10 })).toBe(
false,
);
expect(isInputSlider({ type: "slider", min: "0", max: 10 })).toBe(false);
expect(isInputSlider({ type: "select", min: 0, max: 10 })).toBe(false);
});
});
Expand All @@ -100,15 +86,9 @@ describe("isSelectFromNotes", () => {
});

it("should return false for invalid selectFromNotes objects", () => {
expect(isSelectFromNotes({ type: "select", source: "notes" })).toBe(
false,
);
expect(
isSelectFromNotes({ type: "select", source: "notes", folder: 123 }),
).toBe(false);
expect(isSelectFromNotes({ type: "note", folder: "some folder" })).toBe(
false,
);
expect(isSelectFromNotes({ type: "select", source: "notes" })).toBe(false);
expect(isSelectFromNotes({ type: "select", source: "notes", folder: 123 })).toBe(false);
expect(isSelectFromNotes({ type: "note", folder: "some folder" })).toBe(false);
});
});
describe("MultiSelectFixedSchema", () => {
Expand All @@ -118,7 +98,10 @@ describe("MultiSelectFixedSchema", () => {
source: "fixed",
multi_select_options: ["Option 1", "Option 2", "Option 3"],
};
expect(parse(MultiselectSchema, validSchema)).toEqual(validSchema);
expect(parse(MultiselectSchema, validSchema)).toEqual({
allowUnknownValues: false,
...validSchema,
});
});

it("should not validate an invalid multiselect fixed schema with missing properties", () => {
Expand Down
10 changes: 3 additions & 7 deletions src/core/formDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ export function isSelectFromNotes(input: unknown): input is selectFromNotes {
return is(SelectFromNotesSchema, input);
}

export function isInputNoteFromFolder(
input: unknown,
): input is inputNoteFromFolder {
export function isInputNoteFromFolder(input: unknown): input is inputNoteFromFolder {
return is(InputNoteFromFolderSchema, input);
}
export function isInputSelectFixed(input: unknown): input is inputSelectFixed {
Expand Down Expand Up @@ -103,6 +101,7 @@ export type EditableInput = {
options?: { value: string; label: string }[];
multi_select_options?: string[];
query?: string;
allowUnknownValues?: boolean;
};

export type EditableFormDefinition = FormDefinition & {
Expand Down Expand Up @@ -157,10 +156,7 @@ export function isValidFormDefinition(input: unknown): input is FormDefinition {
return true;
}

export function duplicateForm(
formName: string,
forms: (FormDefinition | MigrationError)[],
) {
export function duplicateForm(formName: string, forms: (FormDefinition | MigrationError)[]) {
return pipe(
forms,
A.findFirstMap((f) => {
Expand Down
7 changes: 4 additions & 3 deletions src/exampleModalDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const exampleModalDefinition: FormDefinition = {
input: {
type: "multiselect",
source: "fixed",
allowUnknownValues: false,
multi_select_options: [
"Android",
"iOS",
Expand All @@ -81,12 +82,13 @@ export const exampleModalDefinition: FormDefinition = {
type: "multiselect",
source: "dataview",
query: 'dv.pages("#person").map(p => p.file.name)',
allowUnknownValues: true,
},
},
{
name: "best_fried",
label: "Best friend",
description: "Pick one",
description: "Select of type note from a folder",
input: {
type: "select",
source: "notes",
Expand All @@ -96,8 +98,7 @@ export const exampleModalDefinition: FormDefinition = {
{
name: "dataview_example",
label: "Dataview example",
description:
"Only people matching the dataview query will be shown",
description: "Only people matching the dataview query will be shown",
input: {
type: "dataview",
query: 'dv.pages("#person").filter(p => p.age < 30).map(p => p.file.name)',
Expand Down
18 changes: 14 additions & 4 deletions src/suggesters/MultiSuggest.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { AbstractInputSuggest, App } from 'obsidian'
import { AbstractInputSuggest, App } from "obsidian";

export class MultiSuggest extends AbstractInputSuggest<string> {
content: Set<string>;

constructor(private inputEl: HTMLInputElement, content: Set<string>, private onSelectCb: (value: string) => void, app: App) {
constructor(
private inputEl: HTMLInputElement,
content: Set<string>,
private onSelectCb: (value: string) => void,
app: App,
private allowUnknownValues = false,
) {
super(app, inputEl);
this.content = content;
}

getSuggestions(inputStr: string): string[] {
const lowerCaseInputStr = inputStr.toLocaleLowerCase();
return [...this.content].filter((content) =>
content.toLocaleLowerCase().contains(lowerCaseInputStr)
const candidates =
this.allowUnknownValues && inputStr !== ""
? [...this.content, inputStr]
: Array.from(this.content);
return candidates.filter((content) =>
content.toLocaleLowerCase().contains(lowerCaseInputStr),
);
}

Expand Down
Loading

0 comments on commit 5cfe8ff

Please sign in to comment.