Skip to content

Commit

Permalink
feat: multi select UI builder
Browse files Browse the repository at this point in the history
  • Loading branch information
danielo515 committed Sep 15, 2023
1 parent fe56108 commit 3eff7a8
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 130 deletions.
264 changes: 135 additions & 129 deletions src/core/formDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,91 +5,91 @@
*/

export type FieldType =
| "text"
| "number"
| "date"
| "time"
| "datetime"
| "toggle";
| "text"
| "number"
| "date"
| "time"
| "datetime"
| "toggle";

type selectFromNotes = { type: "select"; source: "notes", folder: string };
type inputSlider = { type: "slider"; min: number, max: number };
type inputNoteFromFolder = { type: "note"; folder: string };
type inputDataviewSource = { type: 'dataview', query: string };
type inputSelectFixed = {
type: "select";
source: "fixed";
options: { value: string; label: string }[];
type: "select";
source: "fixed";
options: { value: string; label: string }[];
}
type basicInput = { type: FieldType };
type multiselect = { type: 'multiselect', source: 'notes', folder: string } | { type: 'multiselect', source: 'fixed', options: string[] }
type inputType =
| basicInput
| inputNoteFromFolder
| inputSlider
| selectFromNotes
| inputDataviewSource
| multiselect
| inputSelectFixed;
| basicInput
| inputNoteFromFolder
| inputSlider
| selectFromNotes
| inputDataviewSource
| multiselect
| inputSelectFixed;

export const FieldTypeReadable: Record<AllFieldTypes, string> = {
"text": "Text",
"number": "Number",
"date": "Date",
"time": "Time",
"datetime": "DateTime",
"toggle": "Toggle",
"note": "Note",
"slider": "Slider",
"select": "Select",
"dataview": "Dataview",
"multiselect": "Multiselect",
"text": "Text",
"number": "Number",
"date": "Date",
"time": "Time",
"datetime": "DateTime",
"toggle": "Toggle",
"note": "Note",
"slider": "Slider",
"select": "Select",
"dataview": "Dataview",
"multiselect": "Multiselect",
} as const;

function isObject(input: unknown): input is Record<string, unknown> {
return typeof input === "object" && input !== null;
return typeof input === "object" && input !== null;
}
export function isDataViewSource(input: unknown): input is inputDataviewSource {
return isObject(input) && input.type === 'dataview' && typeof input.query === 'string';
return isObject(input) && input.type === 'dataview' && typeof input.query === 'string';
}

export function isInputSlider(input: unknown): input is inputSlider {
if (!isObject(input)) {
return false;
}
if ('min' in input && 'max' in input && typeof input.min === 'number' && typeof input.max === 'number' && input.type === 'slider') {
return true;
}
return false
if (!isObject(input)) {
return false;
}
if ('min' in input && 'max' in input && typeof input.min === 'number' && typeof input.max === 'number' && input.type === 'slider') {
return true;
}
return false
}
export function isSelectFromNotes(input: unknown): input is selectFromNotes {
if (!isObject(input)) {
return false;
}
return input.type === "select" && input.source === "notes" && typeof input.folder === "string";
if (!isObject(input)) {
return false;
}
return input.type === "select" && input.source === "notes" && typeof input.folder === "string";
}

export function isInputNoteFromFolder(input: unknown): input is inputNoteFromFolder {
if (!isObject(input)) {
return false;
}
return input.type === "note" && typeof input.folder === "string";
if (!isObject(input)) {
return false;
}
return input.type === "note" && typeof input.folder === "string";
}
export function isInputSelectFixed(input: unknown): input is inputSelectFixed {
if (!isObject(input)) {
return false;
}
return input.type === "select" && input.source === "fixed" && Array.isArray(input.options) && input.options.every((option: unknown) => {
return isObject(option) && typeof option.value === "string" && typeof option.label === "string";
})
if (!isObject(input)) {
return false;
}
return input.type === "select" && input.source === "fixed" && Array.isArray(input.options) && input.options.every((option: unknown) => {
return isObject(option) && typeof option.value === "string" && typeof option.label === "string";
})
}

export type AllFieldTypes = inputType['type']
export type FieldDefinition = {
name: string;
label?: string;
description: string;
input: inputType;
name: string;
label?: string;
description: string;
input: inputType;
}
/**
* FormDefinition is an already valid form, ready to be used in the form modal.
Expand All @@ -102,104 +102,110 @@ export type FieldDefinition = {
* @param type - The type of the field. Can be one of "text", "number", "date", "time", "datetime", "toggle".
*/
export type FormDefinition = {
title: string;
name: string;
fields: FieldDefinition[];
title: string;
name: string;
fields: FieldDefinition[];
};

// 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";
folder?: string;
min?: number;
max?: number;
options?: { value: string; label: string }[];
query?: string;
type: AllFieldTypes;
source?: "notes" | "fixed";
folder?: string;
min?: number;
max?: number;
options?: { value: string; label: string }[];
query?: string;
};

export type EditableFormDefinition = {
title: string;
name: string;
fields: {
name: string;
label?: string;
description: string;
input: EditableInput;
}[];
title: string;
name: string;
fields: {
name: string;
label?: string;
description: string;
input: EditableInput;
}[];
};

export function isValidBasicInput(input: unknown): input is basicInput {
if (!isObject(input)) {
return false;
}
return ["text", "number", "date", "time", "datetime", "toggle"].includes(input.type as string);
if (!isObject(input)) {
return false;
}
return ["text", "number", "date", "time", "datetime", "toggle"].includes(input.type as string);
}

export function isMultiSelect(input: unknown): input is multiselect {
return isObject(input)
&& input.type === 'multiselect'
&& (
(input.source === 'notes' && typeof input.folder === 'string') || (input.source === 'fixed' && Array.isArray(input.options))
)
}

export function isInputTypeValid(input: unknown): input is inputType {
if (isValidBasicInput(input)) {
return true;
} else if (isInputNoteFromFolder(input)) {
return true;
} else if (isInputSlider(input)) {
return true;
} else if (isSelectFromNotes(input)) {
return true;
} else if (isInputSelectFixed(input)) {
return true;
} else if (isDataViewSource(input)) {
return true;
} else {
return false;
}
switch (true) {
case isValidBasicInput(input):
case isInputNoteFromFolder(input):
case isInputSlider(input):
case isSelectFromNotes(input):
case isInputSelectFixed(input):
case isDataViewSource(input):
case isMultiSelect(input):
return true;
default:
return false;

}
}


export function decodeInputType(input: EditableInput): inputType | null {
if (isInputSlider(input)) {
return { type: "slider", min: input.min, max: input.max };
} else if (isSelectFromNotes(input)) {
return { type: "select", source: "notes", folder: input.folder };
} else if (isInputNoteFromFolder(input)) {
return { type: "note", folder: input.folder! };
} else if (isInputSelectFixed(input)) {
return { type: "select", source: "fixed", options: input.options };
} else if (isValidBasicInput(input)) {
return { type: input.type };
} else {
return null;
}
if (isInputSlider(input)) {
return { type: "slider", min: input.min, max: input.max };
} else if (isSelectFromNotes(input)) {
return { type: "select", source: "notes", folder: input.folder };
} else if (isInputNoteFromFolder(input)) {
return { type: "note", folder: input.folder! };
} else if (isInputSelectFixed(input)) {
return { type: "select", source: "fixed", options: input.options };
} else if (isValidBasicInput(input)) {
return { type: input.type };
} else {
return null;
}
}

export function isFieldValid(input: unknown): input is FieldDefinition {
if (!isObject(input)) {
return false;
}
if (typeof input.name !== "string" || input.name.length === 0) {
return false;
}
if (typeof input.description !== "string") {
return false;
}
if (input.label !== undefined && typeof input.label !== "string") {
return false;
}
console.log('basic input fields are valid')
return isInputTypeValid(input.input);
if (!isObject(input)) {
return false;
}
if (typeof input.name !== "string" || input.name.length === 0) {
return false;
}
if (typeof input.description !== "string") {
return false;
}
if (input.label !== undefined && typeof input.label !== "string") {
return false;
}
console.log('basic input fields are valid')
return isInputTypeValid(input.input);
}

export function isValidFormDefinition(input: unknown): input is FormDefinition {
if (!isObject(input)) {
return false;
}
if (typeof input.title !== "string") {
return false;
}
if (typeof input.name !== "string" || input.name === '') {
return false;
}
console.log('basic is valid');
return Array.isArray(input.fields) && input.fields.every(isFieldValid);
if (!isObject(input)) {
return false;
}
if (typeof input.title !== "string") {
return false;
}
if (typeof input.name !== "string" || input.name === '') {
return false;
}
console.log('basic is valid');
return Array.isArray(input.fields) && input.fields.every(isFieldValid);
}
2 changes: 1 addition & 1 deletion src/views/FormBuilder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
</div>
</div>
<div class="flex gap1">
{#if field.input.type === "select"}
{#if field.input.type === "select" || field.input.type === "multiselect"}
{@const source_id = `source_${index}`}
<div class="flex column gap1">
<label for={source_id}>Source</label>
Expand Down
32 changes: 32 additions & 0 deletions src/views/components/inputBuilderMulti.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import FormRow from "./FormRow.svelte";
export let index: number;
export let value: string = "";
$: id = `dataview_${index}`;
</script>

<FormRow label="Dataview Query" {id}>
<span class="modal-form-hint">
This is a <a
href="https://blacksmithgu.github.io/obsidian-dataview/api/intro/"
>Dataview</a
>
query that will be used to populate the input suggestions. You should provide
a query that returns a list of strings, for example:
<pre class="language-js"><code
>dv.pages('#tag').map(p => p.file.name)</code
></pre>
</span>
<textarea
{id}
bind:value
name="dataview_query"
class="form-control"
rows="3"
placeholder="dv.pages('#tag').map(p => p.file.name)"
/>
</FormRow>

<style>
</style>

0 comments on commit 3eff7a8

Please sign in to comment.