From 12e4f76a8b1bfdeaa9a54c3c59ba8967fa70dabf Mon Sep 17 00:00:00 2001 From: JYC333 <22962980+JYC333@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:23:06 +0000 Subject: [PATCH] fix: move setup to DOM implementation to fix knockout issue --- apps/client/src/setup.ts | 178 +++++++++++++++++++++++++++------------ 1 file changed, 125 insertions(+), 53 deletions(-) diff --git a/apps/client/src/setup.ts b/apps/client/src/setup.ts index 3b3a40c336..4a8e467090 100644 --- a/apps/client/src/setup.ts +++ b/apps/client/src/setup.ts @@ -1,68 +1,107 @@ import "jquery"; -import ko from "knockout"; - import utils from "./services/utils.js"; -// TriliumNextTODO: properly make use of below types -// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | ""; -// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop"; +type SetupStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop" | "sync-from-server"; +type SetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | ""; -class SetupModel { - syncInProgress: boolean; - step: ko.Observable; - setupType: ko.Observable; - setupNewDocument: ko.Observable; - setupSyncFromDesktop: ko.Observable; - setupSyncFromServer: ko.Observable; - syncServerHost: ko.Observable; - syncProxy: ko.Observable; - password: ko.Observable; +class SetupController { + private step: SetupStep; + private setupType: SetupType = ""; + private syncPollIntervalId: number | null = null; + private rootNode: HTMLElement; + private setupTypeForm: HTMLFormElement; + private syncFromServerForm: HTMLFormElement; + private setupTypeNextButton: HTMLButtonElement; + private setupTypeInputs: HTMLInputElement[]; + private syncServerHostInput: HTMLInputElement; + private syncProxyInput: HTMLInputElement; + private passwordInput: HTMLInputElement; + private sections: Record; - constructor(syncInProgress: boolean) { - this.syncInProgress = syncInProgress; - this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type"); - this.setupType = ko.observable(""); - this.setupNewDocument = ko.observable(false); - this.setupSyncFromDesktop = ko.observable(false); - this.setupSyncFromServer = ko.observable(false); - this.syncServerHost = ko.observable(); - this.syncProxy = ko.observable(); - this.password = ko.observable(); + constructor(rootNode: HTMLElement, syncInProgress: boolean) { + this.rootNode = rootNode; + this.step = syncInProgress ? "sync-in-progress" : "setup-type"; + this.setupTypeForm = mustGetElement("setup-type-form", HTMLFormElement); + this.syncFromServerForm = mustGetElement("sync-from-server-form", HTMLFormElement); + this.setupTypeNextButton = mustGetElement("setup-type-next", HTMLButtonElement); + this.setupTypeInputs = Array.from(document.querySelectorAll("input[name='setup-type']")); + this.syncServerHostInput = mustGetElement("sync-server-host", HTMLInputElement); + this.syncProxyInput = mustGetElement("sync-proxy", HTMLInputElement); + this.passwordInput = mustGetElement("password", HTMLInputElement); + this.sections = { + "setup-type": mustGetElement("setup-type-section", HTMLElement), + "new-document-in-progress": mustGetElement("new-document-in-progress-section", HTMLElement), + "sync-from-desktop": mustGetElement("sync-from-desktop-section", HTMLElement), + "sync-from-server": mustGetElement("sync-from-server-section", HTMLElement), + "sync-in-progress": mustGetElement("sync-in-progress-section", HTMLElement) + }; + } - if (this.syncInProgress) { - setInterval(checkOutstandingSyncs, 1000); + init() { + this.setupTypeForm.addEventListener("submit", (event) => { + event.preventDefault(); + void this.selectSetupType(); + }); + + this.syncFromServerForm.addEventListener("submit", (event) => { + event.preventDefault(); + void this.finish(); + }); + + for (const input of this.setupTypeInputs) { + input.addEventListener("change", () => { + this.setupType = (input.checked ? input.value : this.getSelectedSetupType()) as SetupType; + this.render(); + }); } + + for (const backButton of document.querySelectorAll("[data-action='back']")) { + backButton.addEventListener("click", () => { + this.back(); + }); + } + const serverAddress = `${location.protocol}//${location.host}`; $("#current-host").html(serverAddress); + + if (this.step === "sync-in-progress") { + this.startSyncPolling(); + } + + this.render(); + this.rootNode.style.display = ""; } - // this is called in setup.ejs - setupTypeSelected() { - return !!this.setupType(); - } + private async selectSetupType() { + if (this.setupType === "new-document") { + this.setStep("new-document-in-progress"); - selectSetupType() { - if (this.setupType() === "new-document") { - this.step("new-document-in-progress"); + await $.post("api/setup/new-document"); + window.location.replace("./setup"); + return; + } - $.post("api/setup/new-document").then(() => { - window.location.replace("./setup"); - }); - } else { - this.step(this.setupType()); + if (this.setupType) { + this.setStep(this.setupType); } } - back() { - this.step("setup-type"); - this.setupType(""); + private back() { + this.setStep("setup-type"); + this.setupType = ""; + + for (const input of this.setupTypeInputs) { + input.checked = false; + } + + this.render(); } - async finish() { - const syncServerHost = this.syncServerHost(); - const syncProxy = this.syncProxy(); - const password = this.password(); + private async finish() { + const syncServerHost = this.syncServerHostInput.value.trim(); + const syncProxy = this.syncProxyInput.value.trim(); + const password = this.passwordInput.value; if (!syncServerHost) { showAlert("Trilium server address can't be empty"); @@ -82,15 +121,38 @@ class SetupModel { }); if (resp.result === "success") { - this.step("sync-in-progress"); - - setInterval(checkOutstandingSyncs, 1000); - hideAlert(); + this.setStep("sync-in-progress"); + this.startSyncPolling(); } else { showAlert(`Sync setup failed: ${resp.error}`); } } + + private setStep(step: SetupStep) { + this.step = step; + this.render(); + } + + private render() { + for (const [step, section] of Object.entries(this.sections) as [SetupStep, HTMLElement][]) { + section.style.display = step === this.step ? "" : "none"; + } + + this.setupTypeNextButton.disabled = !this.setupType; + } + + private getSelectedSetupType(): SetupType { + return (this.setupTypeInputs.find((input) => input.checked)?.value ?? "") as SetupType; + } + + private startSyncPolling() { + if (this.syncPollIntervalId !== null) { + return; + } + + this.syncPollIntervalId = window.setInterval(checkOutstandingSyncs, 1000); + } } async function checkOutstandingSyncs() { @@ -124,9 +186,19 @@ function getSyncInProgress() { return !!parseInt(el.content); } +function mustGetElement(id: string, ctor: T): InstanceType { + const element = document.getElementById(id); + + if (!element || !(element instanceof ctor)) { + throw new Error(`Expected element #${id}`); + } + + return element as InstanceType; +} + addEventListener("DOMContentLoaded", (event) => { const rootNode = document.getElementById("setup-dialog"); - if (!rootNode) return; - ko.applyBindings(new SetupModel(getSyncInProgress()), rootNode); - $("#setup-dialog").show(); + if (!rootNode || !(rootNode instanceof HTMLElement)) return; + + new SetupController(rootNode, getSyncInProgress()).init(); });