From b23aa748e4d279ed9dd4a0ad4f1beefae043bc75 Mon Sep 17 00:00:00 2001 From: Tasha Upchurch Date: Tue, 28 Nov 2023 20:11:19 -0500 Subject: [PATCH] Comprehensive UI and State Management Overhaul in JS Files: Implemented gdeState for centralized state handling, introduced GDELogin class for structured login functionality, enhanced dropdowns with clear and onchange handlers, and updated button setups to align with cloud storage. Included event-driven login/logout mechanism and dynamic UI updates in response to user authentication in ui_modifications.js. In ui_helpers.js, refined button and modal setups for improved interaction, and optimized dropdown management for user-centric UI updates. --- ui_helpers.js | 129 +++++++----- ui_modifications.js | 496 +++++++++++++++++++++++++++++--------------- 2 files changed, 415 insertions(+), 210 deletions(-) diff --git a/ui_helpers.js b/ui_helpers.js index 7e72178..873e236 100644 --- a/ui_helpers.js +++ b/ui_helpers.js @@ -1,15 +1,16 @@ import {app} from "../../../scripts/app.js"; import {api} from "../../../scripts/api.js"; -export function setupButtons(menu, app, api) { +export function setupButtons(gdeState, app, api) { // Save To Git Button + const menu = document.querySelector(".comfy-menu"); const saveButton = document.createElement("button"); - saveButton.textContent = "Save To Git"; + saveButton.textContent = "Save To Cloud"; saveButton.onclick = async () => { try { const json = JSON.stringify(app.graph.serialize(), null, 2); - const userNameValue = menu.querySelector("#branch-name-field").value; - const dropDownSelected = menu.querySelector("#graph-name-field").value; + const userNameValue = gdeState.user; + const graphSelected = gdeState.graph; await api.fetchApi("/gde/git/save", { method: "POST", @@ -18,7 +19,7 @@ export function setupButtons(menu, app, api) { }, body: JSON.stringify({ data: json, - file_name: dropDownSelected, + file_name: graphSelected, user_name: userNameValue }), }); @@ -30,9 +31,11 @@ export function setupButtons(menu, app, api) { // Load From Git Button const loadButton = document.createElement("button"); - loadButton.textContent = "Load From Git"; + loadButton.textContent = "Load From Cloud"; loadButton.onclick = async () => { try { + const userNameValue = gdeState.user; + const graphSelected = gdeState.graph; const response = await api.fetchApi("/gde/git/load", { // needs user_name file_name method: "POST", @@ -40,8 +43,8 @@ export function setupButtons(menu, app, api) { "Content-Type": "application/json", }, body: JSON.stringify({ - user_name: menu.querySelector("#branch-name-field").value, - file_name: menu.querySelector("#graph-name-field").value + user_name: userNameValue, + file_name: graphSelected } ), @@ -56,9 +59,10 @@ export function setupButtons(menu, app, api) { } -export function setupModal() { +export function setupModal(gdeState) { // Modal const modal = document.createElement("div"); + modal.id = "graph-name-modal"; modal.style.display = "none"; // Initially hidden modal.style.position = "fixed"; modal.style.top = "50%"; @@ -80,7 +84,10 @@ export function setupModal() { const submitButton = document.createElement("button"); submitButton.textContent = "Submit"; let targetDropdownId = ''; // This will store the ID of the dropdown we're targeting - submitButton.onclick = () => { + + modal.submit = () => { + // this is the function that will be called when the submit button is clicked + // it will add the new option to the dropdown and select it const dropdown = document.querySelector(`#${targetDropdownId}`); const newOption = document.createElement("option"); newOption.value = modalInput.value; @@ -89,6 +96,10 @@ export function setupModal() { dropdown.value = modalInput.value; console.log("Submitted value:", modalInput.value); modal.style.display = "none"; // Hide the modal + return modalInput.value; + } + submitButton.onclick = () => { + modal.submit() }; modal.append(submitButton); @@ -99,13 +110,37 @@ export function setupModal() { }; } +HTMLSelectElement.prototype.clearDropDown = function (placeHolder = "") { + // remove everything + while (this.options.length > 0) { + this.remove(0); + } + // only add if there is a placeholder + if (placeHolder != "") { + //add select+type + const defaultOption = document.createElement("option"); + defaultOption.value = ""; + defaultOption.textContent = placeHolder + this.append(defaultOption); -export function setupDropdown(menu, api, placeholder, apiEndpoint, dropdownId, user = null, graph_name = null, onCreateNew = null) { + // Add "Create New" option + const createNewOption = document.createElement("option"); + createNewOption.value = "create_new"; + createNewOption.textContent = "Create New"; + this.append(createNewOption); + } + +}; + + +export function setupDropdown(menu, api, placeholder, apiEndpoint, dropdownId, + user = null, onCreateNew = null, + onChanged = null) { let dropdown = document.getElementById(dropdownId); // If dropdown already exists, clear its options first if (dropdown) { - dropdown.innerHTML = ''; + dropdown.clearDropDown(placeholder) } else { // Create new dropdown if it doesn't exist dropdown = document.createElement("select"); @@ -118,55 +153,49 @@ export function setupDropdown(menu, api, placeholder, apiEndpoint, dropdownId, u dropdown.id = dropdownId; //use comfy-list style - + dropdown.clearDropDown(placeholder) menu.append(dropdown); + + // Add event listener to the dropdown + dropdown.addEventListener("change", (event) => { + // Check if "Create New" option was selected + if (event.target.value === "create_new" && onCreateNew) { + onCreateNew(event, dropdown); + } + if (onChanged) { + onChanged(event, dropdown); + } + + } + ); } - // Add default option - const defaultOption = document.createElement("option"); - defaultOption.value = ""; - defaultOption.textContent = placeholder; - dropdown.append(defaultOption); - // Add "Create New" option - const createNewOption = document.createElement("option"); - createNewOption.value = "create_new"; - createNewOption.textContent = "Create New"; - dropdown.append(createNewOption); - - dropdown.addEventListener("change", (event) => { - // Check if "Create New" option was selected - if (event.target.value === "create_new" && onCreateNew) { - onCreateNew(event, dropdown); - } - - // Check if branch dropdown is the one being changed - if (dropdownId === 'branch-name-field') { - while (dropdown.options.length > 2) { - dropdown.remove(2); - } - setTimeout(async () => { - await someFunction(dropdownId, api, apiEndpoint, placeholder, user, graph_name) - }) - } - }); + // dropdown.addEventListener("change", (event) => { + // // Check if "Create New" option was selected + // if (event.target.value === "create_new" && onCreateNew) { + // onCreateNew(event, dropdown); + // } + // + // }); setTimeout(async () => { - await someFunction(dropdownId, api, apiEndpoint, placeholder, user, graph_name) - }) + dropdown.clearDropDown(placeholder) + await someFunction(dropdownId, api, apiEndpoint, placeholder, user) + }, 50) + return dropdown; } -export async function someFunction(dropdownId, api, apiEndpoint, placeholder, user = null, graph_name = null) { +export async function someFunction(dropdownId, api, apiEndpoint, placeholder, user = null) { + // this is the function that will be called to fetch the options for the dropdown try { const dropdown = document.getElementById(dropdownId); dropdown.disabled = true; // Disable the file dropdown - + const clientId = api.socket.url.split("=")[1] const queryParams = { - user: user || "" + user: user || "", + client_id: clientId }; - if (graph_name) { - queryParams.graph_name = graph_name; - } const response = await api.fetchApi(apiEndpoint, { method: "POST", @@ -177,6 +206,10 @@ export async function someFunction(dropdownId, api, apiEndpoint, placeholder, us }); const optionsData = await response.json(); optionsData.forEach(optionItem => { + // if the option already exists, don't add it again + if (dropdown.querySelector(`option[value="${optionItem.value}"]`)) { + return; + } const optionElement = document.createElement("option"); optionElement.value = optionItem.value; optionElement.textContent = optionItem.label; diff --git a/ui_modifications.js b/ui_modifications.js index df0c379..c762596 100644 --- a/ui_modifications.js +++ b/ui_modifications.js @@ -2,153 +2,233 @@ import {app} from "../../../scripts/app.js"; import {api} from "../../../scripts/api.js"; import {setupButtons, setupModal, setupDropdown} from './ui_helpers.js'; + +let loggedInEvent = null; +let loggedOutEvent = new Event('loggedOutEvent'); +export let selectedGraph = null; + +//const graphCreateNewEvent = new Event('graphCreateNew'); // create a place to store some variables, including security_token -const gde = { - security_token: null, -} -gde.login_div = document.createElement("div"); -gde.login_div.id = "gde-login"; -gde.usernameField = document.createElement("input"); -gde.usernameField.id = "username-field"; -gde.usernameField.placeholder = "Username"; -//add a logout button -gde.logoutButton = document.createElement("button"); -gde.logoutButton.id = "logout-button"; -gde.logoutButton.innerText = "Logout"; -gde.logoutButton.addEventListener("click", async () => { - //clear the token - gde.security_token = null; - //remove the logout button - gde.logoutButton.remove(); - //add the login button and fields - gde.login_div.append(gde.usernameField); - gde.login_div.append(gde.passwordField); - gde.login_div.append(gde.loginButton); - // clear the graph dropdown - const graphDropdown = document.getElementById("graph-name-field"); - if (graphDropdown) { - // remove all options without removing the first two - while (graphDropdown.options.length > 2) { - graphDropdown.remove(2); - } - } - // clear the branch dropdown - const branchDropdown = document.getElementById("branch-name-field"); - if (branchDropdown) { - // remove all options without removing the first two - while (branchDropdown.options.length > 2) { - branchDropdown.remove(2); - } - } - - // tell the server we logged out - await api.fetchApi("/gde/logout", { - method: "POST", - body: JSON.stringify({ - client_id: api.clientId, - }) - } - ); +class GDELogin { + constructor(api) { + this.api = api; + this.security_token = null; + this.initElements(); } -); -gde.passwordField = document.createElement("input"); -gde.passwordField.id = "password-field"; -gde.passwordField.placeholder = "Password"; -gde.passwordField.type = "password"; -gde.loginButton = document.createElement("button"); -gde.loginButton.id = "login-button"; -gde.loginButton.style.color = "var(--input-text)"; -gde.loginButton.style.backgroundColor = "var(--comfy-input-bg)"; -gde.loginButton.style.borderRadius = "8px"; -gde.loginButton.style.borderColor = "var(--border-color)"; -gde.loginButton.style.borderStyle = "solid"; -gde.loginButton.style.marginTop = "2px"; -gde.loginButton.style.width = "100%"; -gde.loginButton.innerText = "Login"; -gde.loginButton.addEventListener("click", async () => { - const username = document.getElementById("username-field").value; - const password = document.getElementById("password-field").value; - const res = await api.fetchApi("/gde/login", { - method: "POST", - body: JSON.stringify({ - username: username, - password: password, - client_id: api.clientId, - } - ) - } - ) - ; + + initElements() { + this.login_div = this.createElement("div", {id: "gde-login"}); + this.usernameField = this.createInputField("username-field", "Username"); + this.passwordField = this.createInputField("password-field", "Password", "password"); + this.loginButton = this.createButton("login-button", "Login", this.handleLogin.bind(this)); + this.logoutButton = this.createButton("logout-button", "Logout", this.handleLogout.bind(this)); + } + + createInputField(id, placeholder, type = "text") { + return this.createElement("input", { + id: id, + placeholder: placeholder, + type: type, + }); + } + + createButton(id, text, clickHandler) { + const button = this.createElement("button", { + id: id, + innerText: text, + }); + button.addEventListener("click", clickHandler); + return button; + } + + createElement(tag, attributes) { + const element = document.createElement(tag); + for (let attr in attributes) { + element[attr] = attributes[attr]; + } + return element; + } + + async handleLogin() { + const username = this.usernameField.value; + const password = this.passwordField.value; + + gdeState.user = username; + + const res = await this.api.fetchApi("/gde/login", { + method: "POST", + body: JSON.stringify({ + username: username, + password: password, + client_id: this.api.clientId, + }), + }); + if (res.status !== 200) { - alert("Login failed"); + //if res.logged_in is false then the login failed but only need to alert if this.api.clientId is set + if (res.logged_in === false && this.api.clientId) { + alert("Login failed. Please check your username and password and try again."); + } + if (res.logged_in === true) { + this.username = res.user; + gdeState.user = username; + } + } else { - //read the json response, token will be under security - const json = await res.json(); + const security_json = await res.json(); + this.security_token = security_json.security; + this.switchToLogoutView(); - //set the token it is somewhere in the json under security - gde.security_token = json.security; + if (username) { + const event = new CustomEvent('loggedInEvent', + {detail: {'user': username}}); + //loggedInEvent.target = {value:json.user}; + document.dispatchEvent(event); + } else { // client was refreshed user was lost but client_id was not + this.usernameField.value = security_json.user; + loggedInEvent.target.value = security_json.user; + document.dispatchEvent(loggedInEvent); - //now that we are logged in remove the login button and fields - gde.usernameField.remove(); - gde.passwordField.remove(); - gde.loginButton.remove(); + } + const welcomeMessage = this.createElement("div", { + id: "welcome-message", + innerText: `Welcome ${this.usernameField.value}` + }); + this.login_div.append(welcomeMessage); + } - gde.login_div.append(gde.logoutButton); + } + async handleLogout() { + await this.api.fetchApi("/gde/logout", { + method: "POST", + body: JSON.stringify({ + client_id: this.api.clientId, + }), + }); + document.dispatchEvent(loggedOutEvent); + this.clearDropdowns(); + this.switchToLoginView(); + const welcomeMessage = document.getElementById("welcome-message"); + if (welcomeMessage) welcomeMessage.remove(); + + } + + switchToLogoutView() { + this.usernameField.remove(); + this.passwordField.remove(); + this.loginButton.remove(); + this.login_div.append(this.logoutButton); + } + + switchToLoginView() { + this.logoutButton.remove(); + this.login_div.append(this.usernameField, this.passwordField, this.loginButton); + } + + clearDropdowns() { + this.clearDropdown("graph-name-field"); + //this.clearDropdown("branch-name-field"); + } + + clearDropdown(id) { + const dropdown = document.getElementById(id); + if (dropdown) { + while (dropdown.options.length > 2) { + dropdown.remove(2); + } } } -); -gde.get_server_state = async (client_id) => { - // check with the server to see if we have a valid client_id - const response = await api.fetchApi("/gde/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - client_id: client_id, - }), - }); - const data = await response.json(); - return data; + + async getServerState(client_id) { + // check with the server to see if we have a valid client_id + const response = await api.fetchApi("/gde/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + client_id: client_id, + }), + }); + + const data = await response.json(); + return data; + } + + async setupLoginForm() { + const menu = document.querySelector(".comfy-menu"); + // create a div in the menu for this extension + + //need to check with server for the status of our current api.clientId + const cid = api.clientId || api.socket.url.split("=")[1]; + const server_state = await this.getServerState(cid).then((server_state) => { + const logged_in = server_state.logged_in; + gdeState.user = server_state.user; + + const separator = document.createElement("hr"); + separator.style.margin = "20px 0"; + separator.style.width = "100%"; + menu.append(separator); + menu.append(this.login_div); + + if (logged_in) { + this.login_div.append(this.logoutButton); + + } else { + this.login_div.append(this.usernameField); + this.login_div.append(this.passwordField); + this.login_div.append(this.loginButton); + + } + }); + } +} + +const gdeLogin = new GDELogin(api); +let gdeState = { + // this is for holding the state of the ui like what is selected etc + graph: null, + branch: null, + user: null, } // create a dummy function -gde.setup_login_form = async () => { - const menu = document.querySelector(".comfy-menu"); - // create a div in the menu for this extension - - - //need to check with server for the status of our current api.clientId - const cid = api.clientId || api.socket.url.split("=")[1]; - const server_state = await gde.get_server_state(cid).then((server_state) => { - const logged_in = server_state.logged_in; - - const separator = document.createElement("hr"); - separator.style.margin = "20px 0"; - separator.style.width = "100%"; - menu.append(separator); - menu.append(gde.login_div); - - if (logged_in) { - gde.login_div.append(gde.logoutButton); - - } else { - gde.login_div.append(gde.usernameField); - gde.login_div.append(gde.passwordField); - gde.login_div.append(gde.loginButton); - - } - }); - -} -gde.hijack_queueprompt = async () => { - // is this actually needed? -} +// gde.setup_login_form = async () => { +// const menu = document.querySelector(".comfy-menu"); +// // create a div in the menu for this extension +// +// +// //need to check with server for the status of our current api.clientId +// const cid = api.clientId || api.socket.url.split("=")[1]; +// const server_state = await gde.get_server_state(cid).then((server_state) => { +// const logged_in = server_state.logged_in; +// +// const separator = document.createElement("hr"); +// separator.style.margin = "20px 0"; +// separator.style.width = "100%"; +// menu.append(separator); +// menu.append(gde.login_div); +// +// if (logged_in) { +// gde.login_div.append(gde.logoutButton); +// +// } else { +// gde.login_div.append(gde.usernameField); +// gde.login_div.append(gde.passwordField); +// gde.login_div.append(gde.loginButton); +// +// } +// }); +// +// } +// gde.hijack_queueprompt = async () => { +// // is this actually needed? +// } app.registerExtension({ name: "GDE.UI.RemoveDefaultButtons", async setup(app) { @@ -161,60 +241,152 @@ app.registerExtension({ name: "GDE.UI.Login", async setup() { setTimeout(async () => { - await gde.setup_login_form(); - await gde.hijack_queueprompt(); + await gdeLogin.setupLoginForm(); + //await gde.hijack_queueprompt(); }, 10); } }); +function SetupGraphDropDown(graphDropdown, + event, + menu, + handleGraphCreateNew, + handleGraphSelect) { + // event may come in as null, so we need to populate it with default values + if (!event) { + event = { + detail: { + user: "main", + graph: "default.json", + branch: "main" + }, + target: { + value: "main" + } + } + gdeState.user = "main" + } + // graphDropdown.clearDropDown(); + graphDropdown.clearDropDown("Select Graph"); + const selectedGraph = null + graphDropdown = setupDropdown(menu, api, + "Select Graph", + "/gde/git/user_graphs", + "graph-name-field", + gdeState.user, + handleGraphCreateNew, + handleGraphSelect); + // modify graphDropdown to change gdeState.graph when it is changed + graphDropdown.addEventListener("change", (event) => { + const selectedGraph = event.target.value; + if (selectedGraph && selectedGraph !== gdeState.graph) { + gdeState.graph = selectedGraph; + const graphChangeEvent = new CustomEvent('graphChangeEvent', { + detail: { + graph: selectedGraph + } + }); + document.dispatchEvent(graphChangeEvent); + } + }); + return graphDropdown; +} + app.registerExtension({ name: "GDE.UI.Git", async setup() { setupModal(); + setupButtons(gdeState, app, api); + + const separator = document.createElement("hr"); const menu = document.querySelector(".comfy-menu"); separator.style.margin = "20px 0"; separator.style.width = "100%"; menu.append(separator); - setupButtons(menu, app, api); - const handleBranchCreateNew = (event, dropdown) => { - // Custom logic for when "Create New" is selected in the branch dropdown - showModal('branch-name-field'); // Show the modal for creating a new branch - dropdown.value = ""; // Reset the dropdown value - // Clear the file dropdown - const fileDropdown = document.getElementById("graph-name-field"); - if (fileDropdown) { - // remove all options without removing the first two - while (fileDropdown.options.length > 2) { - fileDropdown.remove(2); - } - } - }; - const handleGraphCreateNew = (event, dropdown) => { - // Custom logic for when "Create New" is selected in the branch dropdown - showModal('graph-name-field'); // Show the modal for creating a new branch - dropdown.value = ""; // Reset the dropdown value - // Clear the file dropdown - const fileDropdown = document.getElementById("graph-name-field"); - }; - setupDropdown(menu, api, "Select Branch", "/gde/git/users", "branch-name-field", null, null, handleBranchCreateNew); - const branchDropdown = document.getElementById("branch-name-field"); + // const handleBranchCreateNew = (event, dropdown) => { + // // Custom logic for when "Create New" is selected in the branch dropdown + // showModal('branch-name-field'); // Show the modal for creating a new branch + // dropdown.value = ""; // Reset the dropdown value + // // Clear the file dropdown + // const fileDropdown = document.getElementById("graph-name-field"); + // + // + // }; - branchDropdown.addEventListener("change", (event) => { - const selectedBranch = event.target.value; - if (selectedBranch && selectedBranch !== "Create New") { - // Repopulate the file/graph dropdown based on the selected branch - setupDropdown(menu, api, "Select Graph", "/gde/git/user_graphs", "graph-name-field", selectedBranch, null, handleGraphCreateNew); - } + document.addEventListener('loggedInEvent', async (event) => { + gdeState.user = event.detail.user; + //gdeState.graph = event.detail.graph; + graphDropdown = SetupGraphDropDown( + graphDropdown, + event, + menu, + handleGraphCreateNew, + handleGraphSelect); }); + // no lets call clear dropdown when there is a loggout + document.addEventListener('loggedOutEvent', async (event) => { + graphDropdown.clearDropDown(); + } + ); - setupDropdown(menu, api, "Select Graph", "/gde/git/user_graphs", "graph-name-field", null, null, handleGraphCreateNew); + const handleGraphCreateNew = (event, dropdown) => { + // Custom logic for when "Create New" is selected in the branch dropdown + + showModal('graph-name-field') + dropdown.disabled = true; + + const modal = document.getElementById("graph-name-modal"); + // hijack its onsubmit function + const prev_submit = modal.submit; + //submitButton.onclick + modal.submit = () => { + let graphName = prev_submit(); + gdeState.graph = graphName; + dropdown.disabled = false; + + } + }; + const handleGraphSelect = (event, dropdown) => { + // change gdeState.graph when it is changed + const selectedGraph = event.target.value; + gdeState.graph = selectedGraph; + + }; + + + //setupDropdown(menu, api, "Select Branch", "/gde/git/users", "branch-name-field", null, null, handleBranchCreateNew); + //const branchDropdown = document.getElementById("branch-name-field"); + + // branchDropdown.addEventListener("change", (event) => { + // const selectedBranch = event.target.value; + // if (selectedBranch && selectedBranch !== "Create New") { + // // Repopulate the file/graph dropdown based on the selected branch + // setupDropdown(menu, api, "Select Graph", "/gde/git/user_graphs", "graph-name-field", selectedBranch, null, handleGraphCreateNew); + // } + // + // }); + + + let tmp = setupDropdown(menu, + api, + "Select Graph", + "/gde/git/user_graphs", + "graph-name-field", + gdeState.user, + handleGraphCreateNew, + handleGraphSelect); + let graphDropdown = SetupGraphDropDown(tmp, + null, + menu, + handleGraphCreateNew, + handleGraphSelect); } });