From 040f8f504c282876b7798da00db3ddda9f8a1c9f Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Thu, 2 Nov 2023 08:56:06 +0100 Subject: [PATCH] fix: ManageForms view is now reactive thanks to stores --- src/core/formDefinition.ts | 31 +++++++++- src/main.ts | 42 ++++++------- src/std/index.ts | 12 +++- src/store/store.ts | 40 +++++++++++++ src/views/ManageForms.svelte | 22 ++++--- src/views/ManageFormsView.ts | 94 +++--------------------------- src/views/components/Button.svelte | 1 - 7 files changed, 117 insertions(+), 125 deletions(-) create mode 100644 src/store/store.ts diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts index 93186f1d..d3547345 100644 --- a/src/core/formDefinition.ts +++ b/src/core/formDefinition.ts @@ -1,5 +1,6 @@ import { type Output, is, safeParse } from "valibot"; -import { SelectFromNotesSchema, InputSliderSchema, InputNoteFromFolderSchema, InputDataviewSourceSchema, InputSelectFixedSchema, InputBasicSchema, MultiselectSchema, InputTypeSchema, FieldDefinitionSchema, FormDefinitionLatestSchema, FieldListSchema, FormDefinitionBasicSchema } from "./formDefinitionSchema"; +import { SelectFromNotesSchema, InputSliderSchema, InputNoteFromFolderSchema, InputDataviewSourceSchema, InputSelectFixedSchema, InputBasicSchema, MultiselectSchema, InputTypeSchema, FieldDefinitionSchema, FormDefinitionLatestSchema, FieldListSchema, FormDefinitionBasicSchema, MigrationError } from "./formDefinitionSchema"; +import { A, O, pipe } from "@std"; //=========== Types derived from schemas type selectFromNotes = Output; type inputSlider = Output; @@ -120,3 +121,31 @@ export function isValidFormDefinition(input: unknown): input is FormDefinition { console.log('fields are valid'); return true; } + +export function duplicateForm(formName: string, forms: (FormDefinition | MigrationError)[]) { + return pipe( + forms, + A.findFirstMap((f) => { + if (f instanceof MigrationError) { + return O.none; + } + if (f.name === formName) { + return O.some(f); + } + return O.none; + }), + O.map((f) => { + let newName = f.name + '-copy'; + let i = 1; + while (forms.some((f) => f.name === newName)) { + newName = f.name + '-copy-' + i; + i++; + } + return { ...f, name: newName }; + }), + O.map((f) => { + return [...forms, f]; + }), + O.getOrElse(() => forms) + ) +} diff --git a/src/main.ts b/src/main.ts index 8ee272f1..8e7949e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import { log_notice } from "./utils/Log"; import * as E from "fp-ts/Either"; import { pipe } from "fp-ts/function"; import * as A from "fp-ts/Array" +import { settingsStore } from "./store/store"; type ViewType = typeof EDIT_FORM_VIEW | typeof MANAGE_FORMS_VIEW; @@ -40,6 +41,7 @@ function notifyMigrationErrors(errors: MigrationError[]) { // This is the plugin entrypoint export default class ModalFormPlugin extends Plugin { public settings: ModalFormSettings | undefined; + private unsubscribeSettingsStore: () => void = () => { }; // This things will be setup in the onload function rather than constructor public api!: PublicAPI; @@ -55,17 +57,10 @@ export default class ModalFormPlugin extends Plugin { return this.settings?.formDefinitions.some((form) => form.name === formName) ?? false; } - async duplicateForm(form: FormDefinition) { - const newForm = { ...form }; - newForm.name = form.name + '-copy'; - let i = 1; - while (this.formExists(newForm.name)) { - newForm.name = form.name + '-copy-' + i; - i++; - } - await this.saveForm(newForm); - } - + /** + * Opens the form in the editor. + * @returns + */ async editForm(formName: string) { // By reading settings from the disk we get a copy of the form // effectively preventing any unexpected side effects to the running configuration @@ -97,22 +92,15 @@ export default class ModalFormPlugin extends Plugin { // go back to manage forms and refresh it await this.activateView(MANAGE_FORMS_VIEW); } - async deleteForm(formName: string) { - // This should never happen, but because obsidian plugin life-cycle we can not guarantee that the settings are loaded - if (!this.settings) { - throw new ModalFormError('Settings not found') - } - this.settings.formDefinitions = this.settings.formDefinitions.filter((form) => form.name !== formName); - await this.saveSettings(); - } - closeEditForm() { this.app.workspace.detachLeavesOfType(EDIT_FORM_VIEW); } - onunload() { } + onunload() { + this.unsubscribeSettingsStore(); + } async activateView(viewType: ViewType, state?: FormDefinition) { const { workspace } = this.app; @@ -175,10 +163,16 @@ export default class ModalFormPlugin extends Plugin { } async onload() { - this.settings = await this.getSettings(); - if (this.settings.formDefinitions.length === 0) { - this.settings.formDefinitions.push(exampleModalDefinition); + const settings = await this.getSettings(); + if (settings.formDefinitions.length === 0) { + settings.formDefinitions.push(exampleModalDefinition); } + settingsStore.set(settings); + this.unsubscribeSettingsStore = settingsStore.subscribe((s) => { + console.log('settings changed', s) + this.settings = s; + this.saveSettings(s) + }); this.api = new API(this.app, this); this.registerView(EDIT_FORM_VIEW, (leaf) => new EditFormView(leaf, this)); this.registerView(MANAGE_FORMS_VIEW, (leaf) => new ManageFormsView(leaf, this)); diff --git a/src/std/index.ts b/src/std/index.ts index 50363fed..132d4c0e 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -1,5 +1,6 @@ import { pipe as p } from "fp-ts/function"; -import { partitionMap, partition, map as mapArr } from "fp-ts/Array"; +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 { BaseSchema, Output, ValiError, parse as parseV } from "valibot"; import { Semigroup, concatAll } from "fp-ts/Semigroup"; @@ -10,7 +11,10 @@ export const pipe = p export const A = { partitionMap, partition, + findFirst, + findFirstMap, map: mapArr, + filter } export const E = { @@ -25,6 +29,12 @@ export const E = { bimap, } +export const O = { + map: mapO, + getOrElse: getOrElseOpt, + some, none, +} + export const parse = tryCatchK(parseV, (e: unknown) => e as ValiError) diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 00000000..0317ecc8 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,40 @@ +import { A } from '@std'; +import { pipe } from 'fp-ts/lib/function'; +import { FormDefinition, duplicateForm } from 'src/core/formDefinition'; +import { MigrationError } from 'src/core/formDefinitionSchema'; +import { ModalFormSettings, getDefaultSettings } from 'src/core/settings'; +import { writable, derived } from 'svelte/store'; + +const settings = writable({ ...getDefaultSettings() }); +export const formsStore = derived(settings, ($settings) => pipe($settings.formDefinitions, A.filter((form): form is FormDefinition => !(form instanceof MigrationError)))); +const { subscribe, update, set } = settings + +export const invalidFormsStore = derived(settings, ($settings) => { + return pipe($settings.formDefinitions, + A.filter((form): form is MigrationError => form instanceof MigrationError)); +}) + +export const settingsStore = { + subscribe, + set, + updateForm(name: string, form: FormDefinition) { + update((s): ModalFormSettings => { + const forms = s.formDefinitions.map((f) => { + if (f.name === name) return form; + return f; + }); + return { ...s, formDefinitions: forms }; + }); + }, + removeForm(name: string) { + update((s): ModalFormSettings => { + const forms = s.formDefinitions.filter((f) => f.name !== name); + return { ...s, formDefinitions: forms }; + }); + }, + duplicateForm(formName: string) { + update((s): ModalFormSettings => { + return { ...s, formDefinitions: duplicateForm(formName, s.formDefinitions) } + }); + } +} diff --git a/src/views/ManageForms.svelte b/src/views/ManageForms.svelte index 4b0d1645..cd5eb961 100644 --- a/src/views/ManageForms.svelte +++ b/src/views/ManageForms.svelte @@ -3,19 +3,17 @@ import KeyValue from "./components/KeyValue.svelte"; import Button from "./components/Button.svelte"; import { MigrationError } from "src/core/formDefinitionSchema"; - import { is } from "valibot"; - import { isLeft } from "fp-ts/lib/Either"; import { E } from "@std"; - import { right } from "fp-ts/lib/Separated"; + import { Readable } from "svelte/store"; export let createNewForm: () => void; export let deleteForm: (formName: string) => void; - export let duplicateForm: (form: FormDefinition) => void; + export let duplicateForm: (formName: string) => void; export let editForm: (formName: string) => void; export let copyFormToClipboard: (form: FormDefinition) => void; - export let forms: FormDefinition[]; - export let invalidForms: MigrationError[] = []; + export let forms: Readable; + export let invalidForms: Readable; function handleDeleteForm(formName: string) { const confirmed = confirm( `Are you sure you want to delete ${formName}?`, @@ -31,7 +29,7 @@ } function handleDuplicateForm(form: FormDefinition) { console.log(`Duplicating ${form.name}`); - duplicateForm(form); + duplicateForm(form.name); } function handleCopyForm(form: FormDefinition) { console.log(`Copying ${form.name}`); @@ -43,9 +41,9 @@

Manage forms

- {#if invalidForms.length} + {#if $invalidForms.length}

Please take a look at the invalid forms section for details and @@ -55,7 +53,7 @@

- {#each forms as form} + {#each $forms as form}

{form.name}

@@ -102,10 +100,10 @@
{/each} - {#if invalidForms.length} + {#if $invalidForms.length}
- {#each invalidForms as form} + {#each $invalidForms as form}

{form.name}

{#each form.fieldErrors as error} diff --git a/src/views/ManageFormsView.ts b/src/views/ManageFormsView.ts index f715a2bf..b1bc4dfe 100644 --- a/src/views/ManageFormsView.ts +++ b/src/views/ManageFormsView.ts @@ -1,10 +1,8 @@ import { FormDefinition } from "src/core/formDefinition"; -import { MigrationError } from "src/core/formDefinitionSchema"; -import ManageForms from './ManageForms.svelte' +import ManageForms from './ManageForms.svelte'; import ModalFormPlugin from "../main"; -import * as A from 'fp-ts/Array' -import { ItemView, Notice, Setting, WorkspaceLeaf } from "obsidian"; -import { E, pipe } from "@std"; +import { ItemView, Notice, WorkspaceLeaf } from "obsidian"; +import { formsStore, invalidFormsStore, settingsStore } from "src/store/store"; export const MANAGE_FORMS_VIEW = "modal-form-manage-forms-view"; @@ -32,22 +30,11 @@ export class ManageFormsView extends ItemView { const container = this.containerEl.children[1] || this.containerEl.createDiv(); container.empty(); - const settings = await this.plugin.getSettings(); - const allForms = settings.formDefinitions; - const { left: invalidForms, right: forms } = pipe( - allForms, - A.partitionMap((form) => { - if (form instanceof MigrationError) { - return E.left(form); - } else { - return E.right(form); - } - })); this.component = new ManageForms({ target: container, props: { - forms, - invalidForms, + forms: formsStore, + invalidForms: invalidFormsStore, createNewForm: () => { this.plugin.createNewForm(); }, @@ -55,10 +42,10 @@ export class ManageFormsView extends ItemView { this.plugin.editForm(formName); }, deleteForm: (formName: string) => { - this.plugin.deleteForm(formName); + settingsStore.removeForm(formName) }, - duplicateForm: (form: FormDefinition) => { - this.plugin.duplicateForm(form); + duplicateForm: (formName: string) => { + settingsStore.duplicateForm(formName); }, copyFormToClipboard: async (form: FormDefinition) => { await navigator.clipboard.writeText(JSON.stringify(form, null, 2)); @@ -67,71 +54,6 @@ export class ManageFormsView extends ItemView { } }) - // container.createEl("h3", { text: "Manage forms" }); - // this.renderControls(container.createDiv()); - // await this.renderForms(container.createDiv()); - } - - renderControls(root: HTMLElement) { - new Setting(root).addButton((button) => { - button.setButtonText('Add new form').onClick(() => { - this.plugin.createNewForm(); - }) - }) - } - - async renderForms(root: HTMLElement) { - - const settings = await this.plugin.getSettings(); - const forms = settings.formDefinitions; - root.empty(); - const rows = root.createDiv(); - rows.setCssStyles({ display: 'flex', flexDirection: 'column', gap: '10px' }); - forms.forEach((form) => { - if (form instanceof MigrationError) { - return // TODO: UI for migration errors - } - const row = rows.createDiv() - row.setCssStyles({ display: 'flex', flexDirection: 'column', gap: '8px' }) - row.createEl("h4", { text: form.name }); - new Setting(row) - .setName(form.title) - .then((setting) => { - // This moves the separator of the settings container from he top to the bottom - setting.settingEl.setCssStyles({ borderTop: 'none', borderBottom: '1px solid var(--background-modifier-border)' }) - }) - .addButton((button) => { - button.setButtonText("Delete").onClick(async () => { - await this.plugin.deleteForm(form.name); - this.renderForms(root); - }); - button.setIcon('trash') - button.setClass('modal-form-danger') - button.setTooltip('delete form ' + form.name) - } - ) - .addButton((button) => { - button.setClass('modal-form-primary') - return button.setButtonText("Edit").onClick(async () => { - await this.plugin.editForm(form.name); - }); - } - ) - .addButton((btn) => { - btn.setTooltip('duplicate ' + form.name) - btn.setButtonText('Duplicate').onClick(() => { - this.plugin.duplicateForm(form); - }) - }) - .addButton((button) => { - button.setIcon('clipboard-copy') - button.onClick(() => { - navigator.clipboard.writeText(JSON.stringify(form, null, 2)); - new Notice("Form has been copied to the clipboard"); - }); - }) - ; - }) } async onClose() { diff --git a/src/views/components/Button.svelte b/src/views/components/Button.svelte index ed1cf03a..501c1dcd 100644 --- a/src/views/components/Button.svelte +++ b/src/views/components/Button.svelte @@ -16,7 +16,6 @@ let root: HTMLElement; onMount(() => { const btn = new ButtonComponent(root); - console.log({ root, btn }); if (icon) btn.setIcon(icon); if (tooltip) btn.setTooltip(tooltip); if (text) btn.setButtonText(text);