From ea76233ca38110951f671b180f5ce55f8c01fd3c Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Fri, 31 Jul 2026 09:28:01 -0400 Subject: [PATCH 1/2] Add Custom Pages open action Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- .../static/js/admin/admin_custom_pages.js | 56 +++++++++++++++++++ docs/explanation/features/CUSTOM_PAGES.md | 11 +++- functional_tests/test_custom_pages_wiring.py | 28 ++++++---- .../test_admin_custom_pages_open_action.py | 38 +++++++++++++ 5 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 ui_tests/test_admin_custom_pages_open_action.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 9f8af2cc3..e7b780d6e 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -95,7 +95,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.250.105" +VERSION = "0.250.106" IS_DEVELOPMENT = is_development_env_enabled() SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') diff --git a/application/single_app/static/js/admin/admin_custom_pages.js b/application/single_app/static/js/admin/admin_custom_pages.js index a55685dfc..6cb4678c9 100644 --- a/application/single_app/static/js/admin/admin_custom_pages.js +++ b/application/single_app/static/js/admin/admin_custom_pages.js @@ -195,8 +195,11 @@ function renderCustomPagesTable() { appendTextCell(row, page.show_in_nav ? "Shown" : "Hidden"); const actionCell = document.createElement("td"); + actionCell.className = "text-nowrap"; const isPythonPage = page.source === "python"; + actionCell.appendChild(createCustomPageOpenAction(page)); + const editButton = document.createElement("button"); editButton.type = "button"; editButton.className = "btn btn-sm btn-outline-primary me-1"; @@ -219,6 +222,59 @@ function renderCustomPagesTable() { syncRequestAccessPageButtonState(); } +function createCustomPageOpenAction(page) { + const unavailableReason = getCustomPageOpenUnavailableReason(page); + if (unavailableReason) { + const disabledWrapper = document.createElement("span"); + disabledWrapper.className = "d-inline-block me-1"; + disabledWrapper.title = unavailableReason; + disabledWrapper.tabIndex = 0; + disabledWrapper.setAttribute("aria-label", unavailableReason); + + const disabledButton = document.createElement("button"); + disabledButton.type = "button"; + disabledButton.className = "btn btn-sm btn-outline-secondary"; + disabledButton.disabled = true; + disabledButton.textContent = "Open"; + disabledWrapper.appendChild(disabledButton); + return disabledWrapper; + } + + const openLink = document.createElement("a"); + openLink.className = "btn btn-sm btn-outline-success me-1"; + openLink.href = getCustomPageUrl(page); + openLink.target = "_blank"; + openLink.rel = "noopener noreferrer"; + openLink.textContent = "Open"; + openLink.title = "Open this custom page in a new tab. Route authorization still applies."; + return openLink; +} + +function getCustomPageOpenUnavailableReason(page) { + if (window.customPagesInitiallyEnabled !== true) { + return "Enable Custom Pages, save settings, and restart the app before opening custom pages."; + } + + if (!getCustomPageSlug(page)) { + return "This custom page cannot be opened because it is missing a slug."; + } + + if (page?.enabled === false) { + return "Disabled custom pages are not available to open."; + } + + return ""; +} + +function getCustomPageUrl(page) { + const slug = getCustomPageSlug(page); + return slug ? `/custom/${encodeURIComponent(slug)}` : ""; +} + +function getCustomPageSlug(page) { + return String(page?.slug || "").trim(); +} + function requestAccessPageExists() { return customPages.some(page => (page.slug || "").toLowerCase() === "request-access"); } diff --git a/docs/explanation/features/CUSTOM_PAGES.md b/docs/explanation/features/CUSTOM_PAGES.md index 692512c31..950dac7cb 100644 --- a/docs/explanation/features/CUSTOM_PAGES.md +++ b/docs/explanation/features/CUSTOM_PAGES.md @@ -1,6 +1,6 @@ # Custom Pages -Current version: **0.242.040** +Current version: **0.250.106** Implemented in version: **0.242.023** @@ -34,6 +34,8 @@ Request Access guidance modal and duplicate slug checks implemented in version: Authenticated custom page navigation fix implemented in version: **0.242.040** +Admin Settings Open action implemented in version: **0.250.106** + ## Overview Custom Pages let app teams that deploy SimpleChat add trusted, deployment-time pages under the `/custom` namespace without changing the core application route code. The feature is controlled by the Admin Settings toggle `enable_custom_pages` and fails closed when disabled. @@ -44,6 +46,7 @@ Custom Pages let app teams that deploy SimpleChat add trusted, deployment-time p - Cosmos DB metadata container `custom_pages`, configured in `application/single_app/config.py` - Static file folders under `application/single_app/custom_pages/`: `assets`, `css`, `html`, `js`, `json`, and `python` - Admin Settings metadata designer in `application/single_app/templates/admin_settings.html` +- Admin Settings table actions in `application/single_app/static/js/admin/admin_custom_pages.js` ## Technical Specifications @@ -97,10 +100,14 @@ Version `0.242.037` added post-create guidance for editing the Request Access em Version `0.242.040` changed navigation rendering so Custom Pages appears for any signed-in user only when `custom_pages_nav` contains at least one enabled, visible, authorized page. This allows `access_level=authenticated` pages such as Request Access to render for signed-in users without `User` or `Admin`, while hiding the pane when no custom page is available. +Version `0.250.106` added an Admin Settings table Open action for each configured custom page. The action builds an encoded `/custom/` URL and opens it in a new tab so the existing custom page routes continue to enforce feature enablement, per-page enabled state, access level, allowed roles, and `.html` alias compatibility. Disabled pages and pages unavailable because Custom Pages is not enabled show a disabled Open action with explanatory tooltip copy. + ## Usage Instructions Admins can open Admin Settings > Custom Pages to enable the feature and create metadata for simple static pages. Metadata references files already deployed under `application/single_app/custom_pages/html`, `css`, `js`, `assets`, and `json`. +Admins can use the table's Open action to launch an enabled static or Python-backed page directly from Admin Settings. The action opens `/custom/` in a new tab with the admin's current session, so the same route-level authorization outcome applies as it would from navigation or a direct URL. + For CSS, JavaScript, asset, and JSON references, admins add one file at a time using the Add button in the metadata designer. The backend still persists those values as arrays in the `custom_pages` Cosmos container. When Allowed Roles is blank, any signed-in app user can access the custom page. When Allowed Roles has values, users must have at least one matching role in the current session. `Admin` satisfies `User`, but `Admin` does not satisfy arbitrary custom roles. Unknown role names do not grant access. @@ -125,7 +132,7 @@ The Python-backed example is discovered from code, so it should not be created i ## Testing and Validation -Functional coverage was added in `functional_tests/test_custom_pages_wiring.py`. It validates version/configuration wiring, protected route registration, Admin Settings UI wiring, navigation integration, and the trusted HTML rendering boundary without requiring live Cosmos DB connectivity. +Functional coverage was added in `functional_tests/test_custom_pages_wiring.py`. It validates version/configuration wiring, protected route registration, Admin Settings UI wiring, navigation integration, table Open action behavior, and the trusted HTML rendering boundary without requiring live Cosmos DB connectivity. UI contract coverage for the Admin Settings Open action lives in `ui_tests/test_admin_custom_pages_open_action.py`. Known limitations: diff --git a/functional_tests/test_custom_pages_wiring.py b/functional_tests/test_custom_pages_wiring.py index c9f41b816..702be5e97 100644 --- a/functional_tests/test_custom_pages_wiring.py +++ b/functional_tests/test_custom_pages_wiring.py @@ -1,9 +1,9 @@ # test_custom_pages_wiring.py """ Functional test for Custom Pages wiring. -Version: 0.250.025 +Version: 0.250.106 Implemented in: 0.242.023 -Updated in: 0.250.025 +Updated in: 0.250.106 This test ensures that the Custom Pages feature is wired through settings, navigation, admin metadata management, and fail-closed host routes without @@ -40,7 +40,7 @@ def test_custom_pages_configuration(): config = read_text("application/single_app/config.py") settings = read_text("application/single_app/functions_settings.py") - assert_contains(config, 'VERSION = "0.250.025"', "version bump") + assert_contains(config, 'VERSION = "0.250.106"', "version bump") assert_contains(config, 'cosmos_custom_pages_container_name = "custom_pages"', "custom pages container name") assert_contains(config, 'cosmos_custom_pages_container = cosmos_database.create_container_if_not_exists', "custom pages container creation") assert_contains(settings, "'enable_custom_pages': False", "custom pages disabled default") @@ -57,12 +57,12 @@ def test_custom_pages_routes_and_access_controls(): base_template = read_text("application/single_app/templates/base.html") for route in ( - '@app.route("/custom/", methods=["GET"])', - '@app.route("/custom/.html", methods=["GET"])', - '@app.route("/custom/assets///", methods=["GET"])', - '@app.route("/api/custom//", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])', - '@app.route("/api/admin/custom-pages", methods=["GET"])', - '@app.route("/api/admin/custom-pages/developer-guide", methods=["GET"])', + '@bp.route("/custom/", methods=["GET"])', + '@bp.route("/custom/.html", methods=["GET"])', + '@bp.route("/custom/assets///", methods=["GET"])', + '@bp.route("/api/custom//", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])', + '@bp.route("/api/admin/custom-pages", methods=["GET"])', + '@bp.route("/api/admin/custom-pages/developer-guide", methods=["GET"])', ): assert_contains(routes, route, f"route declaration {route}") @@ -76,7 +76,7 @@ def test_custom_pages_routes_and_access_controls(): assert_contains(routes, "request-access", "request access metadata helper route") assert_contains(routes, "access_request_button_enabled", "access request button settings update") assert_contains(routes, "A custom page with this slug already exists.", "duplicate slug create guard") - assert_contains(app, "register_route_custom_pages(app)", "custom page route registration") + assert_contains(app, "register_route_blueprint('custom_pages', register_route_custom_pages, login_required_blueprint)", "custom page route registration") assert_contains(app, "custom_pages_nav=custom_pages_nav", "custom page navigation context") assert_contains(base_template, "_custom_pages_drawer.html", "custom pages drawer include") @@ -151,6 +151,14 @@ def test_custom_pages_admin_and_navigation_wiring(): assert_contains(admin_js, "setupCustomPageFileListEditors", "custom pages file list editor setup") assert_contains(admin_js, "addCustomPageFile", "custom pages file list add action") assert_contains(admin_js, "removeCustomPageFile", "custom pages file list remove action") + assert_contains(admin_js, "createCustomPageOpenAction", "custom page table open action") + assert_contains(admin_js, "getCustomPageUrl", "custom page open URL builder") + assert_contains(admin_js, "`/custom/${encodeURIComponent(slug)}`", "encoded custom page open URL") + assert_contains(admin_js, "openLink.target = \"_blank\"", "custom page open action new tab target") + assert_contains(admin_js, "openLink.rel = \"noopener noreferrer\"", "custom page open action tab isolation") + assert_contains(admin_js, "Route authorization still applies.", "custom page open action authorization tooltip") + assert_contains(admin_js, "Disabled custom pages are not available to open.", "custom page disabled open tooltip") + assert_contains(admin_js, "Enable Custom Pages, save settings, and restart the app before opening custom pages.", "custom pages disabled feature open tooltip") assert_contains(admin_js, "textContent", "safe DOM text rendering") drawer_template = read_text("application/single_app/templates/_custom_pages_drawer.html") diff --git a/ui_tests/test_admin_custom_pages_open_action.py b/ui_tests/test_admin_custom_pages_open_action.py new file mode 100644 index 000000000..a9b76aee4 --- /dev/null +++ b/ui_tests/test_admin_custom_pages_open_action.py @@ -0,0 +1,38 @@ +# test_admin_custom_pages_open_action.py +""" +UI contract test for the Admin Settings Custom Pages Open action. +Version: 0.250.106 +Implemented in: 0.250.106 + +This test ensures the Custom Pages table exposes a safe Open action that links +through the existing `/custom/` route and keeps unavailable pages disabled. +""" + +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ADMIN_CUSTOM_PAGES_JS = REPO_ROOT / "application" / "single_app" / "static" / "js" / "admin" / "admin_custom_pages.js" + + +@pytest.mark.ui +def test_admin_custom_pages_table_open_action_contract(): + """Validate the admin Custom Pages table exposes the expected Open action behavior.""" + script = ADMIN_CUSTOM_PAGES_JS.read_text(encoding="utf-8") + + required_markers = [ + "createCustomPageOpenAction(page)", + "openLink.href = getCustomPageUrl(page)", + "openLink.target = \"_blank\"", + "openLink.rel = \"noopener noreferrer\"", + "`/custom/${encodeURIComponent(slug)}`", + "window.customPagesInitiallyEnabled !== true", + "disabledButton.disabled = true", + "Disabled custom pages are not available to open.", + "Route authorization still applies.", + ] + + for marker in required_markers: + assert marker in script, f"Missing Custom Pages Open action marker: {marker}" From 770b5a24bfe3651957b2e1b37653478f7dddde1b Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Fri, 31 Jul 2026 09:48:23 -0400 Subject: [PATCH 2/2] Update release notes for Custom Pages open action Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/explanation/release_notes.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index b902b880e..77e07c7c0 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,16 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/). +### **(v0.250.106)** + +#### User Interface Enhancements + +* **Custom Pages Admin Open Action** + * Added an Open action to the Admin Settings Custom Pages table so administrators can launch enabled static or Python-backed custom pages directly from their metadata row. + * The action opens encoded `/custom/` URLs in a new tab while preserving existing Custom Pages route authorization, enabled-state checks, access-level rules, role restrictions, and `.html` alias compatibility. + * Disabled or unavailable pages now show a disabled Open action with explanatory tooltip copy instead of silently omitting the action. + * (Ref: Closes #951, PR #1131, `admin_custom_pages.js`, `CUSTOM_PAGES.md`) + ### **(v0.250.105)** #### User Interface Enhancements