Rewrite InstanceDetailsDialog

Also misc cleanup. Almost done with this
This commit is contained in:
CenTdemeern1 2025-02-03 17:23:45 +01:00
parent bfd61c2e50
commit f451b1fbc3
7 changed files with 140 additions and 145 deletions

View file

@ -14,7 +14,7 @@ export function parseHost(host: string): { host: string, secure: boolean } | nul
}; };
} }
type AddInstanceDialogData = { export type AddInstanceDialogData = {
host: string, host: string,
secure: boolean, secure: boolean,
autoQueryMetadata: boolean, autoQueryMetadata: boolean,
@ -27,6 +27,7 @@ export class AddInstanceDialog extends FormDialog {
constructor(dialog: HTMLDialogElement, initializeDOM: boolean = true) { constructor(dialog: HTMLDialogElement, initializeDOM: boolean = true) {
super(dialog, findFormOrFail(dialog, ".addInstanceForm")); super(dialog, findFormOrFail(dialog, ".addInstanceForm"));
this.instanceHost = findInputOrFail(this.form, "#instanceHost"); this.instanceHost = findInputOrFail(this.form, "#instanceHost");
this.autoQueryMetadata = findInputOrFail(this.form, "#autoQueryMetadata"); this.autoQueryMetadata = findInputOrFail(this.form, "#autoQueryMetadata");
this.closeButton = findButtonOrFail(this.form, ".close"); this.closeButton = findButtonOrFail(this.form, ".close");
@ -34,6 +35,13 @@ export class AddInstanceDialog extends FormDialog {
if (initializeDOM) this.initializeDOM(); if (initializeDOM) this.initializeDOM();
} }
protected override initializeDOM() {
super.initializeDOM();
this.instanceHost.addEventListener("input", e => this.#getDataIfValid());
this.closeButton.addEventListener("click", e => this.close());
}
#getDataIfValid(): AddInstanceDialogData | null { #getDataIfValid(): AddInstanceDialogData | null {
const parsedHost = parseHost(this.instanceHost.value); const parsedHost = parseHost(this.instanceHost.value);
if (parsedHost === null) { if (parsedHost === null) {
@ -62,16 +70,9 @@ export class AddInstanceDialog extends FormDialog {
}, ONCE); }, ONCE);
} }
protected override initializeDOM() { async present(): Promise<AddInstanceDialogData> {
super.initializeDOM();
this.instanceHost.addEventListener("input", e => this.#getDataIfValid());
this.closeButton.addEventListener("click", e => this.close());
}
async prompt(): Promise<AddInstanceDialogData> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.dialog.addEventListener("close", e => reject(), ONCE); this.cancelOnceClosed(reject);
this.#handleSubmit(resolve); this.#handleSubmit(resolve);
this.open(); this.open();
}); });

View file

@ -1,56 +1,45 @@
import { AddInstanceDialog } from "./add_an_instance.mjs"; import { AddInstanceDialog } from "./add_an_instance.mjs";
import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; import { InstanceDetailsDialog, InstanceDetailsDialogData } from "./confirm_instance_details.mjs";
import { Dialog } from "./dialog.mjs"; import { Dialog } from "./dialog.mjs";
import storageManager, { Instance } from "./storage_manager.mjs"; import storageManager, { Instance } from "./storage_manager.mjs";
export class AddInstanceFlow { export class AddInstanceFlow {
addDialog: AddInstanceDialog; addDialog: AddInstanceDialog;
spinnerDialog: Dialog; spinnerDialog: Dialog;
detailsDialog: HTMLDialogElement; detailsDialog: InstanceDetailsDialog;
constructor( constructor(
addDialog: AddInstanceDialog | HTMLDialogElement, addDialog: AddInstanceDialog | HTMLDialogElement,
spinnerDialog: HTMLDialogElement, spinnerDialog: HTMLDialogElement,
detailsDialog: HTMLDialogElement, detailsDialog: InstanceDetailsDialog | HTMLDialogElement,
) { ) {
if (addDialog instanceof AddInstanceDialog) if (addDialog instanceof AddInstanceDialog)
this.addDialog = addDialog; this.addDialog = addDialog;
else else
this.addDialog = new AddInstanceDialog(addDialog, true); this.addDialog = new AddInstanceDialog(addDialog, true);
this.spinnerDialog = new Dialog(spinnerDialog); this.spinnerDialog = new Dialog(spinnerDialog);
if (detailsDialog instanceof InstanceDetailsDialog)
this.detailsDialog = detailsDialog; this.detailsDialog = detailsDialog;
else
this.detailsDialog = new InstanceDetailsDialog(detailsDialog, true);
} }
async start() { async start() {
const instanceDetailsDialogCallback = (
name: string,
host: string,
hostSecure: boolean,
software: string,
icon: string | null
) => {
const instance: Instance = {
name,
origin: `http${hostSecure ? "s" : ""}://${host}`,
software,
iconURL: icon ?? undefined
};
storageManager.storage.instances.push(instance);
storageManager.save();
console.log("Successfully added new instance:", instance);
};
const {
showInstanceDetailsDialog,
hideInstanceDetailsDialog,
populateInstanceDetailsDialog
} = initializeInstanceDetailsDialog(this.detailsDialog, instanceDetailsDialogCallback);
const { const {
autoQueryMetadata, autoQueryMetadata,
host, host,
secure, secure,
} = await this.addDialog.prompt(); } = await this.addDialog.present();
let detailsDialogData: InstanceDetailsDialogData = {
name: host,
host,
hostSecure: secure,
software: "",
iconURL: null,
};
try { try {
if (!autoQueryMetadata) throw null; // Skip to catch block if (!autoQueryMetadata) throw null; // Skip to catch block
@ -67,12 +56,23 @@ export class AddInstanceFlow {
) )
throw new Error("Invalid API response"); throw new Error("Invalid API response");
populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null); detailsDialogData.name = name;
} catch { detailsDialogData.software = software;
populateInstanceDetailsDialog(host, host, secure, "", null); detailsDialogData.iconURL = iconURL as string | null;
} finally { } catch { }
this.spinnerDialog.close(); this.spinnerDialog.close();
showInstanceDetailsDialog();
} detailsDialogData = await this.detailsDialog.present(detailsDialogData);
const instance: Instance = {
name: detailsDialogData.name,
origin: `http${detailsDialogData.hostSecure ? "s" : ""}://${detailsDialogData.host}`,
software: detailsDialogData.software,
iconURL: detailsDialogData.iconURL ?? undefined
};
storageManager.storage.instances.push(instance);
storageManager.save();
console.log("Successfully added new instance:", instance);
} }
} }

View file

@ -25,7 +25,7 @@
<center class="half-width"> <center class="half-width">
<ol id="instanceList" class="align-start wfit-content"></ol> <ol id="instanceList" class="align-start wfit-content"></ol>
<br> <br>
<button id="showAddInstanceDialog">Add an instance</button> <button id="startAddInstanceFlow">Add an instance</button>
</center> </center>
</div> </div>
<div class="half-width align-self-start"> <div class="half-width align-self-start">
@ -110,6 +110,7 @@ Unchecking this is not recommended, and this option only exists for exceptional
<button type="reset" class="close">Cancel</button> <button type="reset" class="close">Cancel</button>
</form> </form>
</dialog> </dialog>
<dialog id="spinner"><span class="spinner"></span></dialog>
</body> </body>
</html> </html>

View file

@ -1,6 +1,6 @@
import { parseHost } from "./add_an_instance.mjs"; import { parseHost } from "./add_an_instance.mjs";
import { initializeAddInstanceFlow } from "./add_instance_flow.mjs"; import { AddInstanceFlow } from "./add_instance_flow.mjs";
import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs"; import { InstanceDetailsDialog } from "./confirm_instance_details.mjs";
import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs"; import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs";
import storageManager from "./storage_manager.mjs"; import storageManager from "./storage_manager.mjs";
@ -9,14 +9,18 @@ let reordering = false;
let elementBeingDragged: HTMLLIElement | undefined; let elementBeingDragged: HTMLLIElement | undefined;
const mainDialog = findDialogOrFail(document.body, "#mainDialog"); const mainDialog = findDialogOrFail(document.body, "#mainDialog");
const showAddInstanceDialogButton = findButtonOrFail(document.body, "#showAddInstanceDialog"); const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow");
const detailsDialog = findDialogOrFail(document.body, "#instanceDetails");
const addDialog = findDialogOrFail(document.body, "#addInstance"); const addDialog = findDialogOrFail(document.body, "#addInstance");
const spinnerDialog = findDialogOrFail(document.body, "#spinner");
const detailsDialog = findDialogOrFail(document.body, "#instanceDetails");
const instanceList = findOlOrFail(document.body, "#instanceList"); const instanceList = findOlOrFail(document.body, "#instanceList");
const saveButton = findButtonOrFail(document.body, "#save"); const saveButton = findButtonOrFail(document.body, "#save");
const reorderButton = findButtonOrFail(document.body, "#reorder"); const reorderButton = findButtonOrFail(document.body, "#reorder");
showAddInstanceDialogButton.addEventListener("click", e => showAddInstanceDialog()); let instanceDetailsDialog = new InstanceDetailsDialog(detailsDialog, true);
let addInstanceFlow = new AddInstanceFlow(addDialog, spinnerDialog, instanceDetailsDialog);
startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow.start());
saveButton.addEventListener("click", e => { saveButton.addEventListener("click", e => {
storageManager.save(); storageManager.save();
@ -29,17 +33,6 @@ reorderButton.addEventListener("click", () => {
reorderButton.innerText = reordering ? "Finish reordering" : "Reorder"; reorderButton.innerText = reordering ? "Finish reordering" : "Reorder";
}); });
const {
showInstanceDetailsDialog,
hideInstanceDetailsDialog,
populateInstanceDetailsDialog,
} = initializeInstanceDetailsDialog(detailsDialog, () => { });
const {
showAddInstanceDialog,
hideAddInstanceDialog
} = initializeAddInstanceFlow(detailsDialog, addDialog);
updateInstanceList(); updateInstanceList();
storageManager.addSaveCallback(updateInstanceList); storageManager.addSaveCallback(updateInstanceList);

View file

@ -1,81 +1,75 @@
// This file handles the "Confirm instance details" dialog // This file handles the "Confirm instance details" dialog
import { FormDialog, ONCE } from "./dialog.mjs";
import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs"; import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs";
import knownSoftware from "./known_software.mjs"; import knownSoftware from "./known_software.mjs";
const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
export function initializeInstanceDetailsDialog( export type InstanceDetailsDialogData = {
dialog: HTMLDialogElement, name: string,
callback: ( host: string,
instanceName: string, hostSecure: boolean,
instanceHost: string, software: string,
instanceHostSecure: boolean, iconURL: string | null
instanceSoftware: string, };
instanceIcon: string | null
) => void
): {
showInstanceDetailsDialog: () => void,
hideInstanceDetailsDialog: () => void,
populateInstanceDetailsDialog: (
instanceNameValue: string,
instanceHostValue: string,
instanceHostSecureValue: boolean,
instanceSoftwareValue: string,
instanceIconValue: string | null
) => void
} {
const showInstanceDetailsDialog = () => dialog.showModal();
const hideInstanceDetailsDialog = () => dialog.close();
const form = findFormOrFail(dialog, ".instanceDetailsForm"); export class InstanceDetailsDialog extends FormDialog {
const instanceName = findInputOrFail(form, "#instanceName"); protected instanceName: HTMLInputElement;
const instanceHost = findInputOrFail(form, "#instanceHost"); protected instanceHost: HTMLInputElement;
const instanceHostSecure = findInputOrFail(form, "#instanceHostSecure"); protected instanceHostSecure: HTMLInputElement;
const instanceSoftware = findSelectOrFail(form, "#instanceSoftware"); protected instanceSoftware: HTMLSelectElement;
const instanceIcon = findImageOrFail(form, "#instanceIcon"); protected instanceIcon: HTMLImageElement;
const closeButton = findButtonOrFail(form, ".close"); protected closeButton: HTMLButtonElement;
constructor(dialog: HTMLDialogElement, initializeDOM: boolean = true) {
super(dialog, findFormOrFail(dialog, ".instanceDetailsForm"));
this.instanceName = findInputOrFail(this.form, "#instanceName");
this.instanceHost = findInputOrFail(this.form, "#instanceHost");
this.instanceHostSecure = findInputOrFail(this.form, "#instanceHostSecure");
this.instanceSoftware = findSelectOrFail(this.form, "#instanceSoftware");
this.instanceIcon = findImageOrFail(this.form, "#instanceIcon");
this.closeButton = findButtonOrFail(this.form, ".close");
if (initializeDOM) this.initializeDOM();
}
protected override initializeDOM() {
super.initializeDOM();
for (const [name, software] of Object.entries(knownSoftware.software)) { for (const [name, software] of Object.entries(knownSoftware.software)) {
const option = new Option(software.name, name); const option = new Option(software.name, name);
instanceSoftware.appendChild(option); this.instanceSoftware.appendChild(option);
} }
instanceIcon.src = blankImage; this.instanceIcon.src = blankImage;
const populateInstanceDetailsDialog = ( this.closeButton.addEventListener("click", e => this.close());
instanceNameValue: string, }
instanceHostValue: string,
instanceHostSecureValue: boolean, #handleSubmit(data: InstanceDetailsDialogData, resolve: (data: InstanceDetailsDialogData) => void) {
instanceSoftwareValue: string, this.form.addEventListener("submit", e => {
instanceIconValue: string | null data.name = this.instanceName.value;
) => { data.host = this.instanceHost.value;
instanceName.value = instanceNameValue; data.hostSecure = this.instanceHostSecure.checked;
instanceHost.value = instanceHostValue; data.software = this.instanceSoftware.value;
instanceHostSecure.checked = instanceHostSecureValue; resolve(data);
instanceSoftware.value = instanceSoftwareValue; this.close();
instanceIcon.src = instanceIconValue ?? blankImage; }, ONCE);
}; }
form.addEventListener("submit", e => { async present(data: InstanceDetailsDialogData): Promise<InstanceDetailsDialogData> {
callback( this.instanceName.value = data.name;
instanceName.value, this.instanceHost.value = data.host;
instanceHost.value, this.instanceHostSecure.checked = data.hostSecure;
instanceHostSecure.checked, this.instanceSoftware.value = data.software;
instanceSoftware.value, this.instanceIcon.src = data.iconURL ?? blankImage;
instanceIcon.src
); return new Promise((resolve, reject) => {
form.reset(); this.cancelOnceClosed(reject);
}); this.#handleSubmit(data, resolve);
this.open();
closeButton.addEventListener("click", e => { });
instanceIcon.src = blankImage; }
hideInstanceDetailsDialog();
});
return {
showInstanceDetailsDialog,
hideInstanceDetailsDialog,
populateInstanceDetailsDialog
};
} }

View file

@ -3,9 +3,10 @@ import { findButtonOrFail, findDialogOrFail, findFormOrFail, findInputOrFail, fi
import knownSoftware from "./known_software.mjs"; import knownSoftware from "./known_software.mjs";
import storageManager from "./storage_manager.mjs"; import storageManager from "./storage_manager.mjs";
const radioButtonName = "instanceSelect"; const RADIO_BUTTON_NAME = "instanceSelect";
let addInstanceFlow: AddInstanceFlow | undefined; let addInstanceFlow: AddInstanceFlow | undefined;
const mainDialog = findDialogOrFail(document.body, "#mainDialog"); const mainDialog = findDialogOrFail(document.body, "#mainDialog");
const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow"); const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow");
const addDialog = findDialogOrFail(document.body, "#addInstance"); const addDialog = findDialogOrFail(document.body, "#addInstance");
@ -16,20 +17,6 @@ const redirectButton = findButtonOrFail(document.body, "#redirect");
const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways"); const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways");
const pathText = findPreOrFail(document.body, "#path"); const pathText = findPreOrFail(document.body, "#path");
startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start());
redirectButton.addEventListener("click", e => {
// Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default
redirect(getSelectedOption()!);
});
redirectAlwaysButton.addEventListener("click", e => {
// Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default
const option = getSelectedOption()!;
setAutoRedirect(option);
redirect(option);
});
// Don't bother initializing if we're performing autoredirect // Don't bother initializing if we're performing autoredirect
if (!autoRedirect()) { if (!autoRedirect()) {
createInstanceSelectOptions(); createInstanceSelectOptions();
@ -44,6 +31,20 @@ if (!autoRedirect()) {
mainDialog.show(); mainDialog.show();
}; };
startAddInstanceFlowButton.addEventListener("click", e => addInstanceFlow?.start());
redirectButton.addEventListener("click", e => {
// Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default
redirect(getSelectedOption()!);
});
redirectAlwaysButton.addEventListener("click", e => {
// Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default
const option = getSelectedOption()!;
setAutoRedirect(option);
redirect(option);
});
function updateNoInstanceHint() { function updateNoInstanceHint() {
findParagraphOrFail(document.body, "#no-instance").style.display = findParagraphOrFail(document.body, "#no-instance").style.display =
storageManager.storage.instances.length > 0 storageManager.storage.instances.length > 0
@ -60,7 +61,7 @@ function createInstanceSelectOptions() {
radio.id = instance.origin; radio.id = instance.origin;
radio.value = instance.origin; radio.value = instance.origin;
radio.type = "radio"; radio.type = "radio";
radio.name = radioButtonName; radio.name = RADIO_BUTTON_NAME;
const label = document.createElement("label"); const label = document.createElement("label");
label.htmlFor = instance.origin; label.htmlFor = instance.origin;
label.innerText = instance.name + " "; label.innerText = instance.name + " ";
@ -107,7 +108,7 @@ function getTargetPath(): string {
function getSelectedOption(): string | null { function getSelectedOption(): string | null {
try { try {
return findInputOrFail(instanceSelectForm, `input[name="${radioButtonName}"]:checked`).value; return findInputOrFail(instanceSelectForm, `input[name="${RADIO_BUTTON_NAME}"]:checked`).value;
} catch { } catch {
return null; return null;
} }

View file

@ -1,4 +1,5 @@
export const ONCE = { once: true }; export const ONCE = { once: true };
export const CANCELLED = Symbol("Cancelled");
export class Dialog { export class Dialog {
protected dialog: HTMLDialogElement; protected dialog: HTMLDialogElement;
@ -19,6 +20,10 @@ export class Dialog {
close() { close() {
this.dialog.close(); this.dialog.close();
} }
protected cancelOnceClosed(reject: (reason?: any) => void) {
this.dialog.addEventListener("close", e => reject(CANCELLED), ONCE);
}
}; };
export class FormDialog extends Dialog { export class FormDialog extends Dialog {