From 266becd4e42514fdc13b785970061e076827e4e4 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sun, 15 Mar 2026 00:57:18 +0100 Subject: [PATCH] Use Shown instead of hidden in incoming shares UI --- integ_tests/test_sharing.py | 108 +++++++++++++++--- radicale/web/internal_data/index.html | 23 ++-- .../js/scenes/IncomingSharingScene.js | 37 ++++-- 3 files changed, 128 insertions(+), 40 deletions(-) diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py index 56439ec3..ecc667c9 100644 --- a/integ_tests/test_sharing.py +++ b/integ_tests/test_sharing.py @@ -48,7 +48,9 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") ).to_have_count(1) expect( - page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='ro']") + page.locator( + "tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='ro']" + ) ).to_be_visible() page.once("dialog", lambda dialog: dialog.accept()) page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) @@ -62,7 +64,9 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") ).to_have_count(1) expect( - page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='rw']") + page.locator( + "tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='rw']" + ) ).to_be_visible() page.once("dialog", lambda dialog: dialog.accept()) page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) @@ -89,7 +93,9 @@ def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> Non page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") ).to_have_count(1) expect( - page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='ro']") + page.locator( + "tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='ro']" + ) ).to_be_visible() page.once("dialog", lambda dialog: dialog.accept()) page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) @@ -105,7 +111,9 @@ def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> Non page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") ).to_have_count(1) expect( - page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='rw']") + page.locator( + "tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='rw']" + ) ).to_be_visible() page.once("dialog", lambda dialog: dialog.accept()) page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) @@ -196,7 +204,9 @@ def test_edit_share_by_token(page: Page, radicale_server: str) -> None: page.click('button[data-name="sharebytoken"]') page.click('#newshare button[data-name="submit"]') expect( - page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='ro']") + page.locator( + "tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='ro']" + ) ).to_be_visible() # Edit to RW @@ -207,7 +217,9 @@ def test_edit_share_by_token(page: Page, radicale_server: str) -> None: # Verify RW expect( - page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='rw']") + page.locator( + "tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='rw']" + ) ).to_be_visible() @@ -223,7 +235,9 @@ def test_edit_share_by_map(page: Page, radicale_server: str) -> None: page.locator('input[data-name="sharehref"]').fill("mapped") page.click('#newshare button[data-name="submit"]') expect( - page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='ro']") + page.locator( + "tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='ro']" + ) ).to_be_visible() # Edit map share @@ -239,7 +253,9 @@ def test_edit_share_by_map(page: Page, radicale_server: str) -> None: # Verify changes expect( - page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='rw']") + page.locator( + "tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='rw']" + ) ).to_be_visible() # If disabled, it might not show up or show differently, but our current UI doesn't visually distinguish enabled/disabled in the list yet # Let's verify by re-opening edit scene @@ -307,27 +323,81 @@ def test_incoming_shares(page: Page, radicale_server: str) -> None: # 4. Max sees the incoming share page.click('a[data-name="incomingshares"]') - expect(page.locator('#incomingsharingscene')).to_be_visible() + expect(page.locator("#incomingsharingscene")).to_be_visible() expect( page.locator("tr[data-name='incomingsharerowtemplate']:not(.hidden)") ).to_have_count(1) expect( - page.locator("tr[data-name='incomingsharerowtemplate']:not(.hidden) td[data-name='pathortoken']") - ).to_have_text("mapped") + page.locator( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='pathortoken']" + ) + ).to_have_value("mapped") - # 5. Max makes changes to the hidden flag + # 5. Max enables and shows the share + # Initially, it's disabled and not shown (security by default) expect( - page.locator("tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='hidden']") - ).to_be_checked() - page.uncheck("tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='hidden']") - - expect( - page.locator("tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='hidden']") + page.locator( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='enabled']" + ) ).not_to_be_checked() + expect( + page.locator( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='shown']" + ) + ).not_to_be_checked() + expect( + page.locator( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='shown']" + ) + ).to_be_disabled() + + # Enable it + page.check( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='enabled']" + ) + expect( + page.locator( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='shown']" + ) + ).not_to_be_disabled() + + # Show it + page.check( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='shown']" + ) + expect( + page.locator( + "tr[data-name='incomingsharerowtemplate']:not(.hidden) input[data-name='shown']" + ) + ).to_be_checked() # 6. Assert no error was shown expect(page.locator('#incomingsharingscene span[data-name="error"]')).to_be_hidden() page.click('#incomingsharingscene button[data-name="cancel"]') - expect(page.locator('#incomingsharingscene')).to_be_hidden() + expect(page.locator("#incomingsharingscene")).to_be_hidden() + + +def test_no_incoming_shares_message(page: Page, radicale_server: str) -> None: + # 1. Max logs in + page.goto(radicale_server) + page.fill('#loginscene input[data-name="user"]', "max") + page.fill('#loginscene input[data-name="password"]', "maxpassword") + page.click('button:has-text("Next")') + + # 2. Max goes to incoming shares scene + page.click('a[data-name="incomingshares"]') + expect(page.locator("#incomingsharingscene")).to_be_visible() + + # 3. Verify that the table is hidden and the message is visible + expect(page.locator("#incomingsharingscene table")).to_be_hidden() + expect( + page.locator('#incomingsharingscene [data-name="nosharesmessage"]') + ).to_be_visible() + expect( + page.locator('#incomingsharingscene [data-name="nosharesmessage"]') + ).to_have_text("No incoming shares") + + page.click('#incomingsharingscene button[data-name="cancel"]') + expect(page.locator("#incomingsharingscene")).to_be_hidden() diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index 7d916373..02d32749 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -212,25 +212,28 @@

Incoming Shares

Manage shares that others have shared with you.

- + - + - + - +
+
@@ -249,9 +252,9 @@
Attributes -
diff --git a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js index d9652a60..f1f30493 100644 --- a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +++ b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js @@ -39,6 +39,8 @@ export class IncomingSharingScene { /** @type {HTMLElement} */ let error_element = html_scene.querySelector("[data-name=error]"); /** @type {HTMLElement} */ let tbody = html_scene.querySelector("tbody[data-name=incomingsharesbody]"); /** @type {HTMLElement} */ let template = tbody.querySelector("[data-name=incomingsharerowtemplate]"); + /** @type {HTMLElement} */ let table = html_scene.querySelector("table"); + /** @type {HTMLElement} */ let noshares_message = html_scene.querySelector("[data-name=nosharesmessage]"); let error_handler = new ErrorHandler(error_element); @@ -55,27 +57,28 @@ export class IncomingSharingScene { */ function toggle_share(share, node) { let enabled_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=enabled]")); - let hidden_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=hidden]")); + let shown_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=shown]")); // disable checkboxes while updating enabled_cb.disabled = true; - hidden_cb.disabled = true; + shown_cb.disabled = true; let old_enabled = share.EnabledByUser; let old_hidden = share.HiddenByUser; share.EnabledByUser = enabled_cb.checked; - share.HiddenByUser = hidden_cb.checked; + share.HiddenByUser = !shown_cb.checked; error_handler.clearError(); update_incoming_share(user, password, share, function (error) { enabled_cb.disabled = false; - hidden_cb.disabled = false; + shown_cb.disabled = !share.EnabledByUser; if (error) { error_handler.setError(error); enabled_cb.checked = old_enabled; - hidden_cb.checked = old_hidden; + shown_cb.checked = !old_hidden; + shown_cb.disabled = !old_enabled; } }); } @@ -93,30 +96,42 @@ export class IncomingSharingScene { let prefix = "/" + user + "/"; let filtered_shares = shares.filter(share => share.ShareType === "map" && share.PathOrToken.startsWith(prefix)); + if (filtered_shares.length === 0) { + table.classList.add("hidden"); + noshares_message.classList.remove("hidden"); + } else { + table.classList.remove("hidden"); + noshares_message.classList.add("hidden"); + } + filtered_shares.forEach(function (share) { let node = /** @type {HTMLElement} */(template.cloneNode(true)); node.classList.remove("hidden"); - let pathortoken_td = node.querySelector("[data-name=pathortoken]"); + let pathortoken = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=pathortoken]")); let owner_td = node.querySelector("[data-name=owner]"); let permissions_td = /** @type {HTMLElement} */ (node.querySelector("[data-name=permissions]")); let enabled_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=enabled]")); - let hidden_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=hidden]")); + let shown_cb = /** @type {HTMLInputElement} */ (node.querySelector("[data-name=shown]")); let displayPath = share.PathOrToken.substring(prefix.length); if (displayPath.endsWith("/")) { displayPath = displayPath.substring(0, displayPath.length - 1); } - pathortoken_td.textContent = displayPath; + pathortoken.value = displayPath; owner_td.textContent = share.Owner; displayPermissions(share.Permissions, permissions_td); - enabled_cb.checked = share.EnabledByUser; - hidden_cb.checked = share.HiddenByUser; + let enabled = share.EnabledByUser !== null ? share.EnabledByUser : true; + let shown = share.HiddenByUser !== null ? !share.HiddenByUser : true; + + enabled_cb.checked = enabled; + shown_cb.checked = shown; + shown_cb.disabled = !enabled; enabled_cb.onchange = function () { toggle_share(share, node); }; - hidden_cb.onchange = function () { toggle_share(share, node); }; + shown_cb.onchange = function () { toggle_share(share, node); }; nodes.push(node); tbody.appendChild(node);