From 25e571ad63c62a1258ff44a442b40acce8775751 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sat, 7 Mar 2026 23:18:31 +0100 Subject: [PATCH] Add functionality to add a new share by map --- integ_tests/common.py | 2 + integ_tests/test_sharing.py | 43 +++++++++++++++++ radicale/web/internal_data/NewShareScene.js | 28 +++++++++-- .../web/internal_data/ShareCollectionScene.js | 21 +++++++- radicale/web/internal_data/api.js | 48 +++++++++++++++++++ radicale/web/internal_data/css/main.css | 5 ++ radicale/web/internal_data/index.html | 26 ++++++---- 7 files changed, 158 insertions(+), 15 deletions(-) diff --git a/integ_tests/common.py b/integ_tests/common.py index aedc13be..d55af761 100644 --- a/integ_tests/common.py +++ b/integ_tests/common.py @@ -45,6 +45,8 @@ permit_create_map = true with open(user_path, "w") as f: f.write( """admin:adminpassword +max:maxpassword + """ ) diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py index 9a97f9de..4cb26b88 100644 --- a/integ_tests/test_sharing.py +++ b/integ_tests/test_sharing.py @@ -49,3 +49,46 @@ def test_create_and_delete_share_by_key(page: Page, radicale_server: str) -> Non expect( page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden)") ).to_have_count(0) + + +def test_create_and_delete_share_by_map(page: Page, radicale_server: str) -> None: + login(page, radicale_server) + create_collection(page, radicale_server) + page.hover("article:not(.hidden)") + page.click('article:not(.hidden) a[data-name="share"]', force=True, strict=True) + + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") + ).to_have_count(0) + + page.click('button[data-name="sharebymap"]') + page.locator('input[data-name="shareuser"]').fill("max") + page.locator('input[data-name="sharehref"]').fill("1234") + page.click('#newshare button[data-name="submit"]') + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") + ).to_have_count(1) + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) img[alt='RO']") + ).to_be_visible() + page.once("dialog", lambda dialog: dialog.accept()) + page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") + ).to_have_count(0) + page.click('button[data-name="sharebymap"]') + page.click('label[for="newshare_attr_permissions_rw"]') + page.locator('input[data-name="shareuser"]').fill("max") + page.locator('input[data-name="sharehref"]').fill("1234") + page.click('#newshare button[data-name="submit"]') + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") + ).to_have_count(1) + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) img[alt='RW']") + ).to_be_visible() + page.once("dialog", lambda dialog: dialog.accept()) + page.click('tr:not(.hidden) button[data-name="delete"]', strict=True) + expect( + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") + ).to_have_count(0) diff --git a/radicale/web/internal_data/NewShareScene.js b/radicale/web/internal_data/NewShareScene.js index e0f48afc..b9819a21 100644 --- a/radicale/web/internal_data/NewShareScene.js +++ b/radicale/web/internal_data/NewShareScene.js @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -import { add_share_by_token } from "./api.js"; +import { add_share_by_map, add_share_by_token } from "./api.js"; import { Scene, pop_scene, scene_stack } from "./scene_manager.js"; /** @@ -29,11 +29,15 @@ export class NewShareScene { * @param {string} user * @param {string} password * @param {string} pathMapped + * @param {string} shareType * @param {function():void} onclose */ - constructor(user, password, pathMapped, onclose) { + constructor(user, password, pathMapped, shareType, onclose) { /** @type {HTMLElement} */ let html_scene = document.getElementById("newshare"); /** @type {HTMLFormElement} */ let form = html_scene.querySelector("form"); + /** @type {HTMLElement} */ let sharemapfields = html_scene.querySelector("[data-name=sharemapfields]"); + /** @type {HTMLInputElement} */ let shareuser_input = html_scene.querySelector("[data-name=shareuser]"); + /** @type {HTMLInputElement} */ let sharehref_input = html_scene.querySelector("[data-name=sharehref]"); /** @type {HTMLInputElement} */ let enabled_checkbox = html_scene.querySelector("[data-name=enabled]"); /** @type {HTMLInputElement} */ let hidden_checkbox = html_scene.querySelector("[data-name=hidden]"); let permissions_ro_radio = /** @type {HTMLInputElement} */ (document.getElementById("newshare_attr_permissions_ro")); @@ -62,12 +66,20 @@ export class NewShareScene { let permissions = permissions_rw_radio.checked ? "rw" : "r"; let properties = properties_input.value; - add_share_by_token(user, password, pathMapped, permissions, enabled, hidden, properties, function () { + let callback = function () { if (scene_index !== null) { pop_scene(scene_index - 1); } if (onclose) onclose(); - }); + }; + + if (shareType === "map") { + let share_user = shareuser_input.value; + let href = sharehref_input.value; + add_share_by_map(user, password, pathMapped, permissions, enabled, hidden, properties, share_user, href, callback); + } else { + add_share_by_token(user, password, pathMapped, permissions, enabled, hidden, properties, callback); + } } catch (err) { console.error(err); } @@ -81,6 +93,14 @@ export class NewShareScene { cancel_btn.onclick = oncancel; form.onsubmit = onsubmit; + if (shareType === "map") { + sharemapfields.classList.remove("hidden"); + } else { + sharemapfields.classList.add("hidden"); + } + + shareuser_input.value = ""; + sharehref_input.value = ""; enabled_checkbox.checked = true; hidden_checkbox.checked = false; permissions_ro_radio.checked = true; diff --git a/radicale/web/internal_data/ShareCollectionScene.js b/radicale/web/internal_data/ShareCollectionScene.js index 34f11c63..834c7a41 100644 --- a/radicale/web/internal_data/ShareCollectionScene.js +++ b/radicale/web/internal_data/ShareCollectionScene.js @@ -45,6 +45,9 @@ export class ShareCollectionScene { /** @type {HTMLElement} */ let cancel_btn = html_scene.querySelector("[data-name=cancel]"); /** @type {HTMLElement} */ let share_by_token_btn = html_scene.querySelector( "button[data-name=sharebytoken]" + ); + /** @type {HTMLElement} */ let share_by_map_btn = html_scene.querySelector( + "button[data-name=sharebymap]" ); /** @type {HTMLElement} */ let share_by_token_div = html_scene.querySelector( "div[data-name=sharebytoken]" @@ -65,7 +68,14 @@ export class ShareCollectionScene { } function onsharebytoken() { - let new_share_scene = new NewShareScene(user, password, collection.href, function () { + let new_share_scene = new NewShareScene(user, password, collection.href, "token", function () { + update_share_list(user, password, collection); + }); + push_scene(new_share_scene, false); + } + + function onsharebymap() { + let new_share_scene = new NewShareScene(user, password, collection.href, "map", function () { update_share_list(user, password, collection); }); push_scene(new_share_scene, false); @@ -91,6 +101,15 @@ export class ShareCollectionScene { if (share_by_token_div) share_by_token_div.classList.add("hidden"); } + if (server_features.sharing && server_features.sharing.PermittedCreateCollectionByMap) { + if (share_by_map_btn) { + share_by_map_btn.classList.remove("hidden"); + share_by_map_btn.onclick = onsharebymap; + } + } else { + if (share_by_map_btn) share_by_map_btn.classList.add("hidden"); + } + if (server_features.sharing && server_features.sharing.FeatureEnabledCollectionByMap) { if (share_by_map_div) share_by_map_div.classList.remove("hidden"); } else { diff --git a/radicale/web/internal_data/api.js b/radicale/web/internal_data/api.js index 878ec396..8b1f1cf9 100644 --- a/radicale/web/internal_data/api.js +++ b/radicale/web/internal_data/api.js @@ -519,6 +519,54 @@ export function add_share_by_token( ); } +/** + * @param {string} user + * @param {string} password + * @param {string} pathMapped + * @param {string} permissions + * @param {boolean} enabled + * @param {boolean} hidden + * @param {string} properties + * @param {string} share_user + * @param {string} href + * @param {function():void} callback + */ +export function add_share_by_map( + user, + password, + pathMapped, + permissions, + enabled, + hidden, + properties, + share_user, + href, + callback, +) { + call_sharing_api( + user, + password, + "map/create", + { + PathMapped: pathMapped, + Permissions: permissions, + Enabled: enabled, + Hidden: hidden, + Properties: properties, + User: share_user, + PathOrToken: "/" + share_user + "/" + href, + }, + function (response) { + let json_response = JSON.parse(response); + if (json_response["Status"] !== "success") { + console.error("Failed to create share map: " + (json_response["Status"] || "Unknown error")); + } else { + callback(); + } + }, + ); +} + /** * @param {string} user * @param {string} password diff --git a/radicale/web/internal_data/css/main.css b/radicale/web/internal_data/css/main.css index 5dc06481..7ba2e92b 100644 --- a/radicale/web/internal_data/css/main.css +++ b/radicale/web/internal_data/css/main.css @@ -263,6 +263,11 @@ main { margin-top: 15px; } +#newshare input[type=text] { + margin-bottom: 0 !important; +} + + .deleteconfirmationtxt { text-align: center; font-size: 1em; diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index 0d1c6c82..43fe4ac0 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -185,16 +185,15 @@ - + + + + + + + @@ -206,6 +205,13 @@