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/dependent-fields #288

Merged
merged 21 commits into from
Jun 30, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ meta.json
site/
docs/index.md
docs/changelog.md
coverage
39 changes: 39 additions & 0 deletions docs/blog/posts/NextRelease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Release notes for 1.4x.0
date: 2024-06-29
tags: release-notes
---

This is a very exciting release for me, because it includes one of the features that I have been wanting to implement for a long time, and also one of the most requested features. But it also includes some minor improvements, so let's take those first:

- The placeholder of the label is set by default to the name of the field. This will make it easier for people to understand that the default label value is the name of the field.

Now the big feature:

## **Dependent fields**

As with every new feature, I like to start small, so this first version is very simple.
It just settles the basic foundation and works only with the most basic field types.
This will allow me to gather feedback and improve it in the next releases after making sure that the basic functionality is working as expected.

In this first approach there are not many safeguards either, so you can end up in forms that don't render anything, for example because of with fields that are excluding each other. I don't think this is going to be a big problem in practice, but I will be monitoring the feedback to see if it is necessary to add some kind of validation, or at least some kind of warning.
The reason I am not adding it any limitations in this first version is because flexibility: forms can be called with parameters to omit fields, default values, etc. and I don't want to limit that flexibility.

Here are some screenshots of the feature in action.

Form builder:

![boolean comparison](<conditional-boolean.png>)
![string comparison](<conditional-string.png>)

Form in preview mode with the condition met
![condition met](<condition-met.png>)
with the condition not met
![condition not met](<condition-not-met.png>)

This first iteration is purely visual: just because a field is hidden it does not mean that, if it has a value, is not going to be included in the result. If you fill a field, and then do something that makes it hidden, the value will still be included in the result. I think in practice most people just needs a way to start with several fields hidden, and then show them based on the value of other fields, so I think this is a good first approach.

The wording of the feature is not final, I'm not very satisfied with the current wording, so I'm open to suggestions.
I hope you like it, that it does not introduce too many inconveniences and that it is useful to you.

Please let me know your thoughts and suggestions.
Binary file added docs/blog/posts/condition-met.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/condition-not-met.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/conditional-boolean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/conditional-string.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 11 additions & 10 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { type JestConfigWithTsJest } from 'ts-jest'
import { type JestConfigWithTsJest } from "ts-jest";

const jestConfig: JestConfigWithTsJest = {
// preset: 'ts-jest/presets/default-esm', // or other ESM presets
preset: 'ts-jest', // or other ESM presets
moduleDirectories: ['node_modules', '<rootDir>'],
roots: ['src'],
preset: "ts-jest", // or other ESM presets
moduleDirectories: ["node_modules", "<rootDir>"],
roots: ["src"],
moduleNameMapper: {
'^@std$': '<rootDir>/src/std/$1',
"^@std$": "<rootDir>/src/std/$1",
"^@core$": "<rootDir>/src/core/$1",
},
transformIgnorePatterns: ['node_modules/(?!(svelte)/)'],
transformIgnorePatterns: ["node_modules/(?!(svelte)/)"],
transform: {
'^.+\\.(t|j)sx?$': [
'ts-jest',
"^.+\\.(t|j)sx?$": [
"ts-jest",
{
useESM: true,
},
],
},
}
export default jestConfig
};
export default jestConfig;
53 changes: 2 additions & 51 deletions src/FormModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@
import { App } from "obsidian";
import { FormDefinition } from "src/core/formDefinition";
import { makeFormEngine } from "src/store/formStore";
import InputField from "src/views/components/Form/InputField.svelte";
import ObsidianInputWrapper from "src/views/components/Form/ObsidianInputWrapper.svelte";
import DocumentBlock from "./views/components/Form/DocumentBlock.svelte";
import InputDataview from "./views/components/Form/InputDataview.svelte";
import InputFolder from "./views/components/Form/InputFolder.svelte";
import InputNote from "./views/components/Form/InputNote.svelte";
import InputTag from "./views/components/Form/InputTag.svelte";
import InputTextArea from "./views/components/Form/InputTextArea.svelte";
import MultiSelectField from "./views/components/Form/MultiSelectField.svelte";
import ObsidianSelect from "./views/components/Form/ObsidianSelect.svelte";
import ObsidianToggle from "./views/components/Form/ObsidianToggle.svelte";
import InputSlider from "./views/components/Form/inputSlider.svelte";
import RenderField from "./views/components/Form/RenderField.svelte";
export let app: App;
export let reportFormErrors: (errors: string[]) => void;
export let formEngine: ReturnType<typeof makeFormEngine>;
Expand All @@ -23,43 +12,5 @@
</script>

{#each fields as definition}
{@const { value, errors } = formEngine.addField(definition)}
{#if definition.input.type === "select"}
<ObsidianSelect input={definition.input} field={definition} {value} {errors} />
{:else if definition.input.type === "toggle"}
<ObsidianToggle field={definition} {value} />
{:else if definition.input.type === "folder"}
<InputFolder field={definition} {value} {app} />
{:else if definition.input.type === "dataview"}
<InputDataview field={definition} input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "note"}
<InputNote field={definition} input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "textarea"}
<InputTextArea field={definition} {value} {errors} />
{:else if definition.input.type === "document_block"}
<!-- I need to put this separated to be able to target the correct slot, it does not work inside #if -->
<ObsidianInputWrapper
label={definition.label || definition.name}
description={definition.description}
>
<DocumentBlock field={definition.input} form={formEngine} slot="info" />
</ObsidianInputWrapper>
{:else}
<ObsidianInputWrapper
{errors}
label={definition.label || definition.name}
description={definition.description}
required={definition.isRequired}
>
{#if definition.input.type === "multiselect"}
<MultiSelectField input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "slider"}
<InputSlider input={definition.input} {value} />
{:else if definition.input.type === "tag"}
<InputTag input={definition.input} {value} {errors} {app} />
{:else}
<InputField {value} inputType={definition.input.type} />
{/if}
</ObsidianInputWrapper>
{/if}
<RenderField {definition} model={formEngine.addField(definition)} {formEngine} {app} />
{/each}
6 changes: 3 additions & 3 deletions src/core/findInputDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { A, NonEmptyArray, ParsingFn, parse, pipe } from "@std";
import * as E from "fp-ts/Either";
import { ValiError, BaseSchema } from "valibot";
import { FieldMinimal, FieldMinimalSchema } from "./formDefinitionSchema";
import { BaseSchema, ValiError } from "valibot";
import { AllFieldTypes } from "./formDefinition";
import { InputTypeToParserMap } from "./InputDefinitionSchema";
import { FieldMinimal, FieldMinimalSchema } from "./formDefinitionSchema";
import { InputTypeToParserMap } from "./input/InputDefinitionSchema";

export function stringifyIssues(error: ValiError): NonEmptyArray<string> {
return error.issues.map(
Expand Down
4 changes: 2 additions & 2 deletions src/core/formDefinition.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { MultiselectSchema } from "./InputDefinitionSchema";
import { parse } from "valibot";
import {
isDataViewSource,
isInputNoteFromFolder,
isInputSelectFixed,
isInputSlider,
isSelectFromNotes,
} from "./formDefinition";
import { parse } from "valibot";
import { MultiselectSchema } from "./input/InputDefinitionSchema";

describe("isDataViewSource", () => {
it("should return true for valid inputDataviewSource objects", () => {
Expand Down
30 changes: 17 additions & 13 deletions src/core/formDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type Output, is, safeParse } from "valibot";
import { input } from "@core";
import { A, O, pipe } from "@std";
import { Simplify } from "type-fest";
import { is, safeParse, type Output } from "valibot";
import {
FieldDefinitionSchema,
FormDefinitionLatestSchema,
FieldListSchema,
FormDefinitionBasicSchema,
FormDefinitionLatestSchema,
MigrationError,
} from "./formDefinitionSchema";
import { A, O, pipe } from "@std";
import { Simplify } from "type-fest";
import {
InputBasicSchema,
InputDataviewSourceSchema,
Expand All @@ -25,7 +26,7 @@ import {
inputType,
multiselect,
selectFromNotes,
} from "./InputDefinitionSchema";
} from "./input/InputDefinitionSchema";

export const InputTypeReadable: Record<AllFieldTypes, string> = {
text: "Text",
Expand Down Expand Up @@ -104,17 +105,20 @@ export type EditableInput = {
allowUnknownValues?: boolean;
};

export type EditableField = {
name: string;
label?: string;
description: string;
input: EditableInput;
folder?: string;
options?: { value: string; label: string }[];
condition?: input.Condition;
};

export type EditableFormDefinition = FormDefinition & {
title: string;
name: string;
fields: {
name: string;
label?: string;
description: string;
input: EditableInput;
folder?: string;
options?: { value: string; label: string }[];
}[];
fields: EditableField[];
};

export function isValidBasicInput(input: unknown): input is basicInput {
Expand Down
26 changes: 14 additions & 12 deletions src/core/formDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { input } from "@core";
import { parse, pipe } from "@std";
import * as E from "fp-ts/Either";
import { pipe, parse } from "@std";
import {
object,
literal,
type Output,
is,
ValiError,
array,
string,
optional,
boolean,
is,
literal,
merge,
unknown,
ValiError,
object,
optional,
passthrough,
boolean,
string,
unknown,
type Output,
} from "valibot";
import { FormDefinition } from "./formDefinition";
import { findFieldErrors, stringifyIssues } from "./findInputDefinitionSchema";
import { FormDefinition } from "./formDefinition";
import { InputTypeSchema, nonEmptyString } from "./input/InputDefinitionSchema";
import { ParsedTemplateSchema } from "./template/templateSchema";
import { InputTypeSchema, nonEmptyString } from "./InputDefinitionSchema";

/**
* Here are the core logic around the main domain of the plugin,
Expand All @@ -30,6 +31,7 @@ export const FieldDefinitionSchema = object({
label: optional(string()),
description: string(),
isRequired: optional(boolean()),
condition: optional(input.ConditionSchema),
input: InputTypeSchema,
});
/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * as input from "./InputDefinitionSchema";
export * as input from "./input";
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
toTrimmed,
union,
} from "valibot";
import { AllFieldTypes, AllSources } from "./formDefinition";
import { AllFieldTypes, AllSources } from "../formDefinition";

/**
* Here are the definition for the input types.
Expand Down Expand Up @@ -142,6 +142,8 @@ export const InputTypeSchema = union([
DocumentBlock,
]);

export type Input = Output<typeof InputTypeSchema>;

export const InputTypeToParserMap: Record<AllFieldTypes, ParsingFn<BaseSchema>> = {
number: parseC(InputBasicSchema),
text: parseC(InputBasicSchema),
Expand Down
Loading
Loading