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

View file

@ -1,56 +1,45 @@
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 storageManager, { Instance } from "./storage_manager.mjs";
export class AddInstanceFlow {
addDialog: AddInstanceDialog;
spinnerDialog: Dialog;
detailsDialog: HTMLDialogElement;
detailsDialog: InstanceDetailsDialog;
constructor(
addDialog: AddInstanceDialog | HTMLDialogElement,
spinnerDialog: HTMLDialogElement,
detailsDialog: HTMLDialogElement,
detailsDialog: InstanceDetailsDialog | HTMLDialogElement,
) {
if (addDialog instanceof AddInstanceDialog)
this.addDialog = addDialog;
else
this.addDialog = new AddInstanceDialog(addDialog, true);
this.spinnerDialog = new Dialog(spinnerDialog);
this.detailsDialog = detailsDialog;
if (detailsDialog instanceof InstanceDetailsDialog)
this.detailsDialog = detailsDialog;
else
this.detailsDialog = new InstanceDetailsDialog(detailsDialog, true);
}
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 {
autoQueryMetadata,
host,
secure,
} = await this.addDialog.prompt();
} = await this.addDialog.present();
let detailsDialogData: InstanceDetailsDialogData = {
name: host,
host,
hostSecure: secure,
software: "",
iconURL: null,
};
try {
if (!autoQueryMetadata) throw null; // Skip to catch block
@ -67,12 +56,23 @@ export class AddInstanceFlow {
)
throw new Error("Invalid API response");
populateInstanceDetailsDialog(name, host, secure, software, iconURL as string | null);
} catch {
populateInstanceDetailsDialog(host, host, secure, "", null);
} finally {
this.spinnerDialog.close();
showInstanceDetailsDialog();
}
detailsDialogData.name = name;
detailsDialogData.software = software;
detailsDialogData.iconURL = iconURL as string | null;
} catch { }
this.spinnerDialog.close();
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">
<ol id="instanceList" class="align-start wfit-content"></ol>
<br>
<button id="showAddInstanceDialog">Add an instance</button>
<button id="startAddInstanceFlow">Add an instance</button>
</center>
</div>
<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>
</form>
</dialog>
<dialog id="spinner"><span class="spinner"></span></dialog>
</body>
</html>

View file

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

View file

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

View file

@ -3,9 +3,10 @@ import { findButtonOrFail, findDialogOrFail, findFormOrFail, findInputOrFail, fi
import knownSoftware from "./known_software.mjs";
import storageManager from "./storage_manager.mjs";
const radioButtonName = "instanceSelect";
const RADIO_BUTTON_NAME = "instanceSelect";
let addInstanceFlow: AddInstanceFlow | undefined;
const mainDialog = findDialogOrFail(document.body, "#mainDialog");
const startAddInstanceFlowButton = findButtonOrFail(document.body, "#startAddInstanceFlow");
const addDialog = findDialogOrFail(document.body, "#addInstance");
@ -16,20 +17,6 @@ const redirectButton = findButtonOrFail(document.body, "#redirect");
const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways");
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
if (!autoRedirect()) {
createInstanceSelectOptions();
@ -44,6 +31,20 @@ if (!autoRedirect()) {
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() {
findParagraphOrFail(document.body, "#no-instance").style.display =
storageManager.storage.instances.length > 0
@ -60,7 +61,7 @@ function createInstanceSelectOptions() {
radio.id = instance.origin;
radio.value = instance.origin;
radio.type = "radio";
radio.name = radioButtonName;
radio.name = RADIO_BUTTON_NAME;
const label = document.createElement("label");
label.htmlFor = instance.origin;
label.innerText = instance.name + " ";
@ -107,7 +108,7 @@ function getTargetPath(): string {
function getSelectedOption(): string | null {
try {
return findInputOrFail(instanceSelectForm, `input[name="${radioButtonName}"]:checked`).value;
return findInputOrFail(instanceSelectForm, `input[name="${RADIO_BUTTON_NAME}"]:checked`).value;
} catch {
return null;
}

View file

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