From f73632f8c7fb7888908841efd09a09519edf8ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Diot?= Date: Sat, 14 Sep 2024 18:02:18 +0200 Subject: [PATCH] Enhance QOL in web UI in general + made advancements in easy mode for services --- src/ui/app/models/config.py | 19 +- src/ui/app/routes/plugins.py | 2 +- src/ui/app/routes/services.py | 113 ++++- src/ui/app/static/css/main.css | 8 + src/ui/app/static/img/bxs-chevron-right.svg | 1 + src/ui/app/static/js/pages/instances.js | 1 - src/ui/app/static/js/pages/services.js | 1 - src/ui/app/static/js/plugins-settings.js | 458 +++++++++++++----- src/ui/app/templates/global_config.html | 15 + src/ui/app/templates/menu.html | 18 +- .../templates/models/checkbox_setting.html | 11 +- .../app/templates/models/input_setting.html | 11 +- .../templates/models/plugins_settings.html | 126 ++--- .../models/plugins_settings_easy.html | 214 ++++++++ .../models/plugins_settings_raw.html | 25 +- .../app/templates/models/select_setting.html | 11 +- src/ui/app/templates/service_settings.html | 40 +- src/ui/main.py | 4 + 18 files changed, 821 insertions(+), 257 deletions(-) create mode 100644 src/ui/app/static/img/bxs-chevron-right.svg diff --git a/src/ui/app/models/config.py b/src/ui/app/models/config.py index c1e3ad640..034e03444 100644 --- a/src/ui/app/models/config.py +++ b/src/ui/app/models/config.py @@ -58,23 +58,18 @@ class Config: def get_plugins_settings(self) -> dict: return { - **{k: v for x in self.get_plugins() for k, v in x["settings"].items()}, + **{k: v for x in self.get_plugins().values() for k, v in x["settings"].items()}, **self.__settings, } - def get_plugins(self, *, _type: Literal["all", "external", "ui", "pro"] = "all", with_data: bool = False) -> List[dict]: - plugins = self.__db.get_plugins(_type=_type, with_data=with_data) - plugins.sort(key=itemgetter("name")) + def get_plugins(self, *, _type: Literal["all", "external", "ui", "pro"] = "all", with_data: bool = False) -> dict: + db_plugins = self.__db.get_plugins(_type=_type, with_data=with_data) + db_plugins.sort(key=itemgetter("name")) - general_plugin = None - for plugin in plugins.copy(): - if plugin["id"] == "general": - general_plugin = plugin - plugins.remove(plugin) - break + plugins = {"general": {}} - if general_plugin: - plugins.insert(0, general_plugin) + for plugin in db_plugins.copy(): + plugins[plugin.pop("id")] = plugin return plugins diff --git a/src/ui/app/routes/plugins.py b/src/ui/app/routes/plugins.py index b67f86fd7..214439625 100644 --- a/src/ui/app/routes/plugins.py +++ b/src/ui/app/routes/plugins.py @@ -302,7 +302,7 @@ def plugins_page(): def update_plugins(threaded: bool = False): wait_applying() - plugins = BW_CONFIG.get_plugins(_type="ui", with_data=True) + plugins = BW_CONFIG.get_plugins(_type="ui", with_data=True) # TODO: remember that this returns a dict now for plugin in deepcopy(plugins): if plugin["id"] in new_plugins_ids: flash(f"Plugin {plugin['id']} already exists", "error") diff --git a/src/ui/app/routes/services.py b/src/ui/app/routes/services.py index a203ccdf4..3c0326e1b 100644 --- a/src/ui/app/routes/services.py +++ b/src/ui/app/routes/services.py @@ -1,7 +1,7 @@ from re import match from threading import Thread from time import time -from typing import Dict +from typing import Dict, List from flask import Blueprint, Response, redirect, render_template, request, url_for from flask_login import login_required @@ -18,6 +18,86 @@ def services_page(): return render_template("services.html", services=DB.get_services(with_drafts=True)) +@services.route("/services/convert", methods=["POST"]) +@login_required +def services_convert(): + verify_data_in_form( + data={"services": None}, + err_message="Missing services parameter on /services/convert.", + redirect_url="services", + next=True, + ) + verify_data_in_form( + data={"convert_to": None}, + err_message="Missing convert_to parameter on /services/convert.", + redirect_url="services", + next=True, + ) + + services = request.form["services"].split(",") + if not services: + return handle_error("No services selected.", "services", True) + + convert_to = request.form["convert_to"] + if convert_to not in ("online", "draft"): + return handle_error("Invalid convert_to parameter.", "services", True) + DATA.load_from_file() + + def convert_services(services: List[str], convert_to: str): + wait_applying() + + db_services = DB.get_services(with_drafts=True) + services_to_convert = set() + non_ui_services = set() + non_convertible_services = set() + + for db_service in db_services: + if db_service["id"] in services: + if db_service["method"] != "ui": + non_ui_services.add(db_service["id"]) + continue + if db_service["is_draft"] == (convert_to == "draft"): + non_convertible_services.add(db_service["id"]) + continue + services_to_convert.add(db_service["id"]) + + for non_ui_service in non_ui_services: + DATA["TO_FLASH"].append({"content": f"Service {non_ui_service} is not a UI service and will not be converted.", "type": "error"}) + + for non_convertible_service in non_convertible_services: + DATA["TO_FLASH"].append( + {"content": f"Service {non_convertible_service} is already a {convert_to} service and will not be converted.", "type": "error"} + ) + + if not services_to_convert: + DATA["TO_FLASH"].append({"content": "All selected services could not be found, are not UI services or are already converted.", "type": "error"}) + DATA.update({"RELOADING": False, "CONFIG_CHANGED": False}) + return + + db_config = DB.get_config(with_drafts=True) + for service in services_to_convert: + db_config[f"{service}_IS_DRAFT"] = "yes" if convert_to == "draft" else "no" + + ret = DB.save_config(db_config, "ui", changed=True) + if isinstance(ret, str): + DATA["TO_FLASH"].append({"content": ret, "type": "error"}) + DATA.update({"RELOADING": False, "CONFIG_CHANGED": False}) + return + DATA["TO_FLASH"].append({"content": f"Converted services: {', '.join(services_to_convert)}", "type": "success"}) + DATA["RELOADING"] = False + + DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True}) + Thread(target=convert_services, args=(services, convert_to)).start() + + return redirect( + url_for( + "loading", + next=url_for("services.services_page"), + message=f"Converting service{'s' if len(services) > 1 else ''} {', '.join(services)} to {convert_to}", + ) + ) + + @services.route("/services/delete", methods=["POST"]) @login_required def services_delete(): @@ -32,10 +112,10 @@ def services_delete(): return handle_error("No services selected.", "services", True) DATA.load_from_file() - def delete_services(services): + def delete_services(services: List[str]): wait_applying() - db_config = BW_CONFIG.get_config(methods=False) + db_config = BW_CONFIG.get_config(methods=False, with_drafts=True) db_services = DB.get_services(with_drafts=True) all_drafts = True services_to_delete = set() @@ -71,7 +151,7 @@ def services_delete(): DATA["TO_FLASH"].append({"content": ret, "type": "error"}) DATA.update({"RELOADING": False, "CONFIG_CHANGED": False}) return - DATA["TO_FLASH"].append({"content": f"Deleted services {', '.join(services_to_delete)}", "type": "success"}) + DATA["TO_FLASH"].append({"content": f"Deleted services: {', '.join(services_to_delete)}", "type": "success"}) DATA["RELOADING"] = False DATA.update({"RELOADING": True, "LAST_RELOAD": time(), "CONFIG_CHANGED": True}) @@ -81,7 +161,7 @@ def services_delete(): url_for( "loading", next=url_for("services.services_page"), - message=f"Deleting services {', '.join(services)}", + message=f"Deleting service{'s' if len(services) > 1 else ''} {', '.join(services)}", ) ) @@ -107,7 +187,7 @@ def services_service_page(service: str): mode = request.args.get("mode", "easy") is_draft = variables.pop("IS_DRAFT", "no") == "yes" - def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode: str): # TODO: handle easy and raw modes + def update_service(service: str, variables: Dict[str, str], is_draft: bool, mode: str): # TODO: handle easy mode wait_applying() # Edit check fields and remove already existing ones @@ -121,15 +201,16 @@ def services_service_page(service: str): ignored_multiples = set() # Edit check fields and remove already existing ones - for variable, value in variables.copy().items(): - if (mode == "raw" or variable != "SERVER_NAME") and value == config.get(variable, {"value": None})["value"]: - if match(r"^.+_\d+$", variable): - ignored_multiples.add(variable) - del variables[variable] + if mode != "easy": + for variable, value in variables.copy().items(): + if (mode == "raw" or variable != "SERVER_NAME") and value == config.get(variable, {"value": None})["value"]: + if match(r"^.+_\d+$", variable): + ignored_multiples.add(variable) + del variables[variable] variables = BW_CONFIG.check_variables(variables, config, ignored_multiples=ignored_multiples, new=service == "new", threaded=True) - if was_draft == is_draft and not variables: + if service != "new" and was_draft == is_draft and not variables: content = f"The service {service} was not edited because no values were changed." DATA["TO_FLASH"].append({"content": content, "type": "warning"}) DATA.update({"RELOADING": False, "CONFIG_CHANGED": False}) @@ -181,6 +262,8 @@ def services_service_page(service: str): mode = request.args.get("mode", "easy") search_type = request.args.get("type", "all") + template = request.args.get("template", "high") + db_templates = DB.get_templates() if service == "new": clone = request.args.get("clone", "") if clone: @@ -189,24 +272,30 @@ def services_service_page(service: str): return render_template( "service_settings.html", config=db_config, + templates=db_templates, mode=mode, type=search_type, + current_template=template, ) db_config = DB.get_config(global_only=True, methods=True) return render_template( "service_settings.html", config=db_config, + templates=db_templates, mode=mode, type=search_type, + current_template=template, ) db_config = DB.get_config(methods=True, with_drafts=True, service=service) return render_template( "service_settings.html", config=db_config, + templates=db_templates, mode=mode, type=search_type, + current_template=template, ) diff --git a/src/ui/app/static/css/main.css b/src/ui/app/static/css/main.css index 5d4bab86e..94f9a442e 100644 --- a/src/ui/app/static/css/main.css +++ b/src/ui/app/static/css/main.css @@ -453,3 +453,11 @@ a.badge:hover { background-color: transparent; opacity: 1; /* You can set this to 0 if you want it to fade out */ } + +.template-steps-container { + --bs-breadcrumb-divider: url("../img/bxs-chevron-right.svg"); +} + +.template-steps-container .breadcrumb-item + .breadcrumb-item::before { + padding: 0 1rem 0 1rem; +} diff --git a/src/ui/app/static/img/bxs-chevron-right.svg b/src/ui/app/static/img/bxs-chevron-right.svg new file mode 100644 index 000000000..670e11ad1 --- /dev/null +++ b/src/ui/app/static/img/bxs-chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ui/app/static/js/pages/instances.js b/src/ui/app/static/js/pages/instances.js index 81e55a63b..2e9a76719 100644 --- a/src/ui/app/static/js/pages/instances.js +++ b/src/ui/app/static/js/pages/instances.js @@ -309,7 +309,6 @@ $(document).ready(function () { ], order: [[7, "desc"]], autoFill: false, - colReorder: true, responsive: true, select: { style: "multi+shift", diff --git a/src/ui/app/static/js/pages/services.js b/src/ui/app/static/js/pages/services.js index e7a73cec7..29f6eb7c6 100644 --- a/src/ui/app/static/js/pages/services.js +++ b/src/ui/app/static/js/pages/services.js @@ -293,7 +293,6 @@ $(document).ready(function () { ], order: [[5, "desc"]], autoFill: false, - colReorder: true, responsive: true, select: { style: "multi+shift", diff --git a/src/ui/app/static/js/plugins-settings.js b/src/ui/app/static/js/plugins-settings.js index 4e13714db..f80df5866 100644 --- a/src/ui/app/static/js/plugins-settings.js +++ b/src/ui/app/static/js/plugins-settings.js @@ -1,6 +1,8 @@ $(document).ready(() => { var toastNum = 0; let currentPlugin = "general"; + let usedTemplate = $("#used-template").val(); + let currentTemplate = $("#selected-template").val(); let currentMode = $("#selected-mode").val(); let currentType = $("#selected-type").val(); @@ -9,7 +11,10 @@ $(document).ready(() => { const $pluginTypeSelect = $("#plugin-type-select"); const $pluginKeywordSearch = $("#plugin-keyword-search"); const $pluginDropdownMenu = $("#plugins-dropdown-menu"); - const pluginDropdownItems = $("#plugins-dropdown-menu li.nav-item"); + const $pluginDropdownItems = $("#plugins-dropdown-menu li.nav-item"); + const $templateSearch = $("#template-search"); + const $templateDropdownMenu = $("#templates-dropdown-menu"); + const $templateDropdownItems = $("#templates-dropdown-menu li.nav-item"); const updateUrlParams = (params, removeHash = false) => { const newUrl = new URL(window.location.href); @@ -36,37 +41,64 @@ $(document).ready(() => { // Prepare params for the URL update const params = {}; - if (currentType !== "all") params.type = currentType; + params.mode = currentMode; + if (currentMode === "advanced" && currentType !== "all") + params.type = currentType; + if (currentMode === "easy" && currentTemplate !== "high") + params.template = currentTemplate; // If "easy" is selected, remove the "mode" parameter if (currentMode === "easy") { params.mode = null; // Set mode to null to remove it from the URL - updateUrlParams(params); // Call the function without the hash (keep it intact) + params.type = null; // Remove the type parameter + updateUrlParams(params, true); // Call the function without the hash (keep it intact) } else { // If another mode is selected, update the "mode" parameter - params.mode = currentMode; - updateUrlParams(params); // Keep the mode in the URL + params.template = null; // Remove the template parameter + if (currentMode === "advanced" && currentPlugin !== "general") { + // Update the URL hash to the current plugin (e.g., #plugin-name) + window.location.hash = currentPlugin; + } else if (currentMode === "raw") { + params.type = null; // Remove the type parameter + } + updateUrlParams(params, currentMode === "raw"); // Keep the mode in the URL } }; const handleTabChange = (targetClass) => { - currentPlugin = targetClass.substring(1).replace("navs-plugins-", ""); - // Prepare the params for URL (parameters to be updated in the URL) const params = {}; - if (currentType !== "all") params.type = currentType; if (currentMode !== "easy") params.mode = currentMode; + if (currentType !== "all") params.type = currentType; - // If "general" is selected and a hash exists, remove the hash but keep the parameters - if (currentPlugin === "general" && window.location.hash) { - // Call updateUrlParams with `removeHash = true` to remove the hash fragment - updateUrlParams(params, true); - } else { - // Update the URL hash to the current plugin (e.g., #plugin-name) - window.location.hash = currentPlugin; + if (targetClass.includes("navs-plugins-")) { + currentPlugin = targetClass.substring(1).replace("navs-plugins-", ""); + params.template = null; // Remove the template parameter - // Also update the URL parameters (if any exist) while preserving the hash - updateUrlParams(params); + // If "general" is selected and a hash exists, remove the hash but keep the parameters + if (currentPlugin === "general" && window.location.hash) { + // Call updateUrlParams with `removeHash = true` to remove the hash fragment + updateUrlParams(params, true); + } else { + // Update the URL hash to the current plugin (e.g., #plugin-name) + window.location.hash = currentPlugin; + + // Also update the URL parameters (if any exist) while preserving the hash + updateUrlParams(params); + } + } else if (targetClass.includes("navs-templates-")) { + currentTemplate = targetClass.substring(1).replace("navs-templates-", ""); + params.type = null; // Remove the type parameter + + // If "high" is selected, remove the "template" parameter + if (currentTemplate === "high") { + params.template = null; // Set template to null to remove it from the URL + updateUrlParams(params); // Call the function without the hash (keep it intact) + } else { + // If another template is selected, update the "template" parameter + params.template = currentTemplate; + updateUrlParams(params); // Keep the template in the URL + } } }; @@ -109,7 +141,70 @@ $(document).ready(() => { } }; - const getFormFromSettings = () => { + // Function to validate inputs and display error messages + const validateCurrentStepInputs = (currentStepContainer) => { + let isStepValid = true; + + currentStepContainer.find(".plugin-setting").each(function () { + const $input = $(this); + const value = $input.val().trim(); + const isRequired = $input.prop("required"); + const pattern = $input.attr("pattern"); + const fieldName = + $input.data("field-name") || $input.attr("name") || "This field"; + + let errorMessage = ""; + let isValid = true; + + // Custom error messages + const requiredMessage = + $input.data("required-message") || `${fieldName} is required.`; + const patternMessage = + $input.data("pattern-message") || `Please enter a valid ${fieldName}.`; + + // Check if the field is required and not empty + if (isRequired && value === "") { + errorMessage = requiredMessage; + isValid = false; + } + + // Validate based on pattern if the input is not empty + if (isValid && pattern && value !== "") { + const regex = new RegExp(pattern); + if (!regex.test(value)) { + errorMessage = patternMessage; + isValid = false; + } + } + + // Toggle valid/invalid classes + $input.toggleClass("is-invalid", !isValid); + + // Manage the invalid-feedback element + let $feedback = $input.next(".invalid-feedback"); + if (!$feedback.length) { + $feedback = $('
').insertAfter( + $input, + ); + } + + if (!isValid) { + $feedback.text(errorMessage); + isStepValid = false; + } else { + $feedback.text(""); + } + }); + + if (!isStepValid) { + // Focus the first invalid input + currentStepContainer.find(".is-invalid").first().focus(); + } + + return isStepValid; + }; + + const getFormFromSettings = (elem) => { const form = $("
", { method: "POST", action: window.location.href, @@ -127,31 +222,48 @@ $(document).ready(() => { ); }; + const addChildrenToForm = (form, elem, isEasy = false) => { + elem.find("input, select").each(function () { + const $this = $(this); + const settingName = $this.attr("name"); + const settingType = $this.attr("type"); + const originalValue = $this.data("original"); + let settingValue = $this.val(); + + if ($this.is("select")) { + settingValue = $this.val(); + } else if (settingType === "checkbox") { + settingValue = $this.is(":checked") ? "yes" : "no"; + } + + if (!isEasy && settingValue == originalValue) return; + + appendHiddenInput(form, settingName, settingValue); + }); + }; + // Handle missing CSRF token gracefully const csrfToken = $("#csrf_token").val() || ""; appendHiddenInput(form, "csrf_token", csrfToken); - // TODO: support easy mode - if (currentMode === undefined || currentMode === "advanced") { - $("div[id^='navs-plugins-']") - .find("input, select") - .each(function () { - const $this = $(this); - const settingName = $this.attr("name"); - const settingType = $this.attr("type"); - const originalValue = $this.data("original"); - let settingValue = $this.val(); + if (currentMode === "easy") { + const template = elem.data("template"); + appendHiddenInput(form, "USE_TEMPLATE", template); + addChildrenToForm(form, $(`#navs-templates-${template}`), true); - if ($this.is("select")) { - settingValue = $this.val(); - } else if (settingType === "checkbox") { - settingValue = $this.is(":checked") ? "yes" : "no"; - } + // Append 'IS_DRAFT' if it exists + const $draftInput = $("#is-draft"); + if ($draftInput.length) { + appendHiddenInput(form, "IS_DRAFT", $draftInput.val()); + } - if (settingValue == originalValue) return; - - appendHiddenInput(form, settingName, settingValue); - }); + // Append 'OLD_SERVER_NAME' if it exists + const $oldServerName = $("#old-server-name"); + if ($oldServerName.length) { + appendHiddenInput(form, "OLD_SERVER_NAME", $oldServerName.val()); + } + } else if (currentMode === undefined || currentMode === "advanced") { + addChildrenToForm(form, $("div[id^='navs-plugins-']")); const $draftInput = $("#is-draft"); if ($draftInput.length) { @@ -243,7 +355,7 @@ $(document).ready(() => { const inputValue = e.target.value.toLowerCase(); let visibleItems = 0; - pluginDropdownItems.each(function () { + $pluginDropdownItems.each(function () { const item = $(this); const matches = (currentType === "all" || item.data("type") === currentType) && @@ -258,21 +370,54 @@ $(document).ready(() => { }); if (visibleItems === 0) { - if ($pluginDropdownMenu.find(".no-items").length === 0) { + if ($pluginDropdownMenu.find(".no-plugin-items").length === 0) { $pluginDropdownMenu.append( - '', + '', ); } } else { - $pluginDropdownMenu.find(".no-items").remove(); + $pluginDropdownMenu.find(".no-plugin-items").remove(); } }, 50), ); - // Clear search and "No Item" message when the dropdown is closed - $("#select-plugin").on("hidden.bs.dropdown", () => { + $("#select-template").on("click", () => $templateSearch.focus()); + + $("#template-search").on( + "input", + debounce((e) => { + const inputValue = e.target.value.toLowerCase(); + let visibleItems = 0; + + $templateDropdownItems.each(function () { + const item = $(this); + const matches = item.text().toLowerCase().includes(inputValue); + + item.toggle(matches); + + if (matches) { + visibleItems++; // Increment when an item is shown + } + }); + + if (visibleItems === 0) { + if ($templateDropdownMenu.find(".no-template-items").length === 0) { + $templateDropdownMenu.append( + '', + ); + } + } else { + $templateDropdownMenu.find(".no-template-items").remove(); + } + }, 50), + ); + + $(document).on("hidden.bs.dropdown", "#select-plugin", function () { $("#plugin-search").val("").trigger("input"); - $(".no-items").remove(); + }); + + $(document).on("hidden.bs.dropdown", "#select-template", function () { + $("#template-search").val("").trigger("input"); }); // Attach event listener to handle mode changes when tabs are switched @@ -290,13 +435,22 @@ $(document).ready(() => { }, ); + $('#templates-dropdown-menu button[data-bs-toggle="tab"]').on( + "shown.bs.tab", + (e) => { + handleTabChange($(e.target).data("bs-target")); + }, + ); + $(document).on("input", ".plugin-setting", function () { - const isValid = $(this).data("pattern") - ? new RegExp($(this).data("pattern")).test($(this).val()) - : true; - $(this) - .toggleClass("is-valid", isValid) - .toggleClass("is-invalid", !isValid); + debounce(() => { + const isValid = $(this).attr("pattern") + ? new RegExp($(this).attr("pattern")).test($(this).val()) + : true; + $(this) + .toggleClass("is-valid", isValid) + .toggleClass("is-invalid", !isValid); + }, 100)(); }); $(document).on("focusout", ".plugin-setting", function () { @@ -310,7 +464,7 @@ $(document).ready(() => { updateUrlParams(params); - pluginDropdownItems.each(function () { + $pluginDropdownItems.each(function () { const typeMatches = currentType === "all" || $(this).data("type") === currentType; $(this).toggle(typeMatches); @@ -325,54 +479,44 @@ $(document).ready(() => { } }); - const findMatchingSettings = (keyword) => { - let matchedPlugin = null; - let matchedSettings = $(); - - $("div[id^='navs-plugins-']").each(function () { - const $plugin = $(this); - const pluginId = $plugin.attr("id").replace("navs-plugins-", ""); - const pluginType = $plugin.data("type"); // Get the type of the plugin (core, external, pro) - - // If the currentType filter is not "all" and the plugin's type doesn't match the currentType, skip this plugin - if (currentType !== "all" && pluginType !== currentType) { - return; // Skip this plugin - } - - // Find settings that match the keyword based on label text or input/select name - const matchingSettings = $plugin.find(".form-label").filter(function () { - const $label = $(this); - const settingName = $label.attr("for") || ""; - const labelText = $label.text().toLowerCase(); - - // Find the associated input/select element using the "for" attribute - const $inputElement = $("#" + settingName); - const inputName = $inputElement.attr("name") || ""; - - // Match either the label text or the input/select name - return ( - labelText.includes(keyword) || - inputName.toLowerCase().includes(keyword) - ); - }); - - if (matchingSettings.length > 0) { - matchedPlugin = pluginId; - matchedSettings = matchingSettings.closest(".col-12"); - return false; // Stop searching after finding a plugin with matching settings - } - }); - - return { matchedPlugin, matchedSettings }; - }; - $pluginKeywordSearch.on( "input", debounce((e) => { const keyword = e.target.value.toLowerCase().trim(); if (!keyword) return; - const { matchedPlugin, matchedSettings } = findMatchingSettings(keyword); + let matchedPlugin = null; + let matchedSettings = $(); + + $("div[id^='navs-plugins-']").each(function () { + const $plugin = $(this); + const pluginId = $plugin.attr("id").replace("navs-plugins-", ""); + const pluginType = $plugin.data("type"); // Get the type of the plugin (core, external, pro) + + // If the currentType filter is not "all" and the plugin's type doesn't match the currentType, skip this plugin + if (currentType !== "all" && pluginType !== currentType) { + return; // Skip this plugin + } + + // Find settings that match the keyword based on label text or input/select name + const matchingSettings = $plugin + .find("input, select") + .filter(function () { + const $input = $(this); + const settingName = ($input.attr("name") || "").toLowerCase(); + const label = $input.next("label"); + const labelText = (label.text() || "").toLowerCase(); + + // Match either the label text or the input/select name + return labelText.includes(keyword) || settingName.includes(keyword); + }); + + if (matchingSettings.length > 0) { + matchedPlugin = pluginId; + matchedSettings = matchingSettings.closest(".col-12"); + return false; // Stop searching after finding a plugin with matching settings + } + }); if (matchedPlugin) { // Automatically switch to the plugin tab @@ -586,31 +730,39 @@ $(document).ready(() => { }); $(".save-settings").on("click", function () { - const form = getFormFromSettings(); - // TODO: support easy mode - let minSettings = 4; - if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2; + const form = getFormFromSettings($(this)); + if (currentMode === "easy") { + const currentStep = parseInt($(this).data("current-step")); + const template = $(this).data("template"); + const currentStepId = `navs-steps-${template}-${currentStep}`; + const currentStepContainer = $(`#${currentStepId}`); + const isStepValid = validateCurrentStepInputs(currentStepContainer); + if (!isStepValid) return; + } else { + let minSettings = 4; + if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2; - const draftInput = $("#is-draft"); - const wasDraft = draftInput.data("original") === "yes"; - let isDraft = draftInput.val() === "yes"; - if (currentMode === "raw") - isDraft = form.find("input[name='IS_DRAFT']").val() === "yes"; + const draftInput = $("#is-draft"); + const wasDraft = draftInput.data("original") === "yes"; + let isDraft = draftInput.val() === "yes"; + if (currentMode === "raw") + isDraft = form.find("input[name='IS_DRAFT']").val() === "yes"; - if (form.children().length < minSettings && isDraft === wasDraft) { - alert("No changes detected."); - return; + if (form.children().length < minSettings && isDraft === wasDraft) { + alert("No changes detected."); + return; + } } $(window).off("beforeunload"); form.appendTo("body").submit(); }); - $("#toggle-draft").on("click", function () { + $(".toggle-draft").on("click", function () { const draftInput = $("#is-draft"); const isDraft = draftInput.val() === "yes"; draftInput.val(isDraft ? "no" : "yes"); - $(this).html( + $(".toggle-draft").html( ` ${isDraft ? "Online" : "Draft"}`, @@ -638,6 +790,54 @@ $(document).ready(() => { }); }); + $(document).on("click", ".next-step, .previous-step", function () { + const template = $(this).data("template"); + let currentStep = parseInt($(this).data("current-step")); + const isNext = $(this).hasClass("next-step"); + + // Determine the new step + const newStep = isNext ? currentStep + 1 : currentStep - 1; + const currentStepId = `navs-steps-${template}-${currentStep}`; + const newStepId = `navs-steps-${template}-${newStep}`; + + const currentStepContainer = $(`#${currentStepId}`); + const newTabTrigger = $(`button[data-bs-target="#${newStepId}"]`); + const currentTabTrigger = $(`button[data-bs-target="#${currentStepId}"]`); + + if (newTabTrigger.length) { + if (isNext) { + const isStepValid = validateCurrentStepInputs(currentStepContainer); + + if (!isStepValid) { + // Prevent proceeding to the next step + return; + } + } + + currentTabTrigger + .parent() + .find("div.text-primary") + .removeClass("text-primary") + .addClass("text-muted"); + currentTabTrigger.addClass("disabled"); + + // Activate the new tab + const newTab = new bootstrap.Tab(newTabTrigger[0]); + newTab.show(); + newTabTrigger + .parent() + .find("div.text-muted") + .removeClass("text-muted") + .addClass("text-primary"); + newTabTrigger.removeClass("disabled"); + newTabTrigger[0].scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + }); + $('div[id^="multiple-"]') .filter(function () { return /^multiple-.*-\d+$/.test($(this).attr("id")); @@ -696,9 +896,24 @@ $(document).ready(() => { } }); + if ( + (usedTemplate === "" || usedTemplate === "ui") && + currentMode === "easy" + ) { + $(`button[data-bs-target="#navs-modes-advanced"]`).tab("show"); + } else if (usedTemplate !== "high" && currentMode === "easy") { + $(`button[data-bs-target="#navs-templates-${usedTemplate}"]`).tab("show"); + } + + if (currentMode === "easy" && currentTemplate !== "high") { + $(`button[data-bs-target="#navs-templates-${currentTemplate}"]`).tab( + "show", + ); + } + var hasExternalPlugins = false; var hasProPlugins = false; - pluginDropdownItems.each(function () { + $pluginDropdownItems.each(function () { const type = $(this).data("type"); if (type === "external") { hasExternalPlugins = true; @@ -723,11 +938,13 @@ $(document).ready(() => { if (targetTab.length) targetTab.tab("show"); } - $pluginTypeSelect.trigger("change"); + if (currentType !== "all") { + $pluginTypeSelect.trigger("change"); + } if (currentMode === "advanced") { const serverNameSetting = $("#setting-general-server-name"); - if (!serverNameSetting.val()) { + if (serverNameSetting.val() === "") { if (currentType !== "all") { currentType = "all"; $pluginTypeSelect.val("all"); @@ -766,18 +983,19 @@ $(document).ready(() => { } $(window).on("beforeunload", function (e) { - const form = getFormFromSettings(); - // TODO: support easy mode - let minSettings = 4; - if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2; + const form = getFormFromSettings($(this)); + if (currentMode !== "easy") { + let minSettings = 4; + if (!form.find("input[name='IS_DRAFT']").length) minSettings = 2; - const draftInput = $("#is-draft"); - const wasDraft = draftInput.data("original") === "yes"; - let isDraft = draftInput.val() === "yes"; - if (currentMode === "raw") - isDraft = form.find("input[name='IS_DRAFT']").val() === "yes"; + const draftInput = $("#is-draft"); + const wasDraft = draftInput.data("original") === "yes"; + let isDraft = draftInput.val() === "yes"; + if (currentMode === "raw") + isDraft = form.find("input[name='IS_DRAFT']").val() === "yes"; - if (form.children().length < minSettings && isDraft === wasDraft) return; + if (form.children().length < minSettings && isDraft === wasDraft) return; + } // Cross-browser compatibility (for older browsers) var message = diff --git a/src/ui/app/templates/global_config.html b/src/ui/app/templates/global_config.html index 4768ce762..4fe18c802 100644 --- a/src/ui/app/templates/global_config.html +++ b/src/ui/app/templates/global_config.html @@ -3,6 +3,21 @@ {% set blacklisted_settings = get_blacklisted_settings(true) %} {% set service_method = "ui" %} + {% set plugin_types = { + "core": { + "icon": "", + "title-class": " border-dark" + }, + "external": { + "icon": "", + "title-class": " border-secondary", + "text-class": " text-secondary fw-bold" + }, + "pro": { + "title-class": " border-primary", + "text-class": " text-primary fw-bold shine" + } + } %} {% include "models/plugins_settings.html" %} {% endblock %} diff --git a/src/ui/app/templates/menu.html b/src/ui/app/templates/menu.html index e2decfde4..4854fab2d 100644 --- a/src/ui/app/templates/menu.html +++ b/src/ui/app/templates/menu.html @@ -73,21 +73,21 @@
- {% for plugin in plugins %} - {% with not_pro_pro_plugin = not is_pro_version and plugin['type'] == "pro" %} - {% if not_pro_pro_plugin or plugin['page'] %} - {% endfor %} @@ -65,15 +67,8 @@ {% if current_endpoint != "global-config" %}
{% set is_draft = config.get('IS_DRAFT', {}).get('value', 'no') %} - -
-{% set plugin_types = { - "core": { - "icon": "", - "title-class": " border-dark" - }, - "external": { - "icon": "", - "title-class": " border-secondary text-secondary fw-bold" - }, - "pro": { - "title-class": " border-primary text-primary fw-bold shine" - } -} %}
- {% for plugin in plugins %} - {% set filtered_settings = get_filtered_settings(plugin["settings"], current_endpoint == "global-config") %} + {% for plugin, plugin_data in plugins.items() %} + {% set filtered_settings = get_filtered_settings(plugin_data["settings"], current_endpoint == "global-config") %} {% if filtered_settings %} -