From c16e9c7c883207bd2876402b7607621c6790bc44 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sat, 14 Mar 2026 00:11:24 +0100 Subject: [PATCH] Display permissions as pill --- integ_tests/test_sharing.py | 16 ++++---- radicale/web/internal_data/css/main.css | 18 ++++++++- radicale/web/internal_data/index.html | 8 ++-- .../js/scenes/CreateEditShareScene.js | 5 +-- .../js/scenes/IncomingSharingScene.js | 5 ++- .../js/scenes/ShareCollectionScene.js | 25 +++--------- .../web/internal_data/js/utils/permissions.js | 38 +++++++++++++++++++ 7 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 radicale/web/internal_data/js/utils/permissions.js diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py index b1f6de95..56439ec3 100644 --- a/integ_tests/test_sharing.py +++ b/integ_tests/test_sharing.py @@ -48,7 +48,7 @@ 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) img[alt='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 +62,7 @@ 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) img[alt='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 +89,7 @@ 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) img[alt='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 +105,7 @@ 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) img[alt='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 +196,7 @@ 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) img[alt='RO']") + page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='ro']") ).to_be_visible() # Edit to RW @@ -207,7 +207,7 @@ def test_edit_share_by_token(page: Page, radicale_server: str) -> None: # Verify RW expect( - page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) img[alt='RW']") + page.locator("tr[data-name='sharetokenrowtemplate']:not(.hidden) span[data-name='rw']") ).to_be_visible() @@ -223,7 +223,7 @@ 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) img[alt='RO']") + page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) span[data-name='ro']") ).to_be_visible() # Edit map share @@ -239,7 +239,7 @@ def test_edit_share_by_map(page: Page, radicale_server: str) -> None: # Verify changes expect( - page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden) img[alt='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 diff --git a/radicale/web/internal_data/css/main.css b/radicale/web/internal_data/css/main.css index 94ae598e..de3c9210 100644 --- a/radicale/web/internal_data/css/main.css +++ b/radicale/web/internal_data/css/main.css @@ -501,4 +501,20 @@ button.inline { #logoutview span { padding: 0 5px; } -} \ No newline at end of file +} + +.pill { + display: inline-block; + padding: 0.25em 0.6em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.375rem; + background-color: #6c757d; + color: #fff; + margin: 0 2px; + width: 1.4em; +} diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index 02674a5f..7d916373 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -159,8 +159,7 @@ - RWRO + rwro @@ -185,8 +184,7 @@ - RWRO + rwro @@ -227,7 +225,7 @@ - + rwro diff --git a/radicale/web/internal_data/js/scenes/CreateEditShareScene.js b/radicale/web/internal_data/js/scenes/CreateEditShareScene.js index 24b1c5a2..2125024d 100644 --- a/radicale/web/internal_data/js/scenes/CreateEditShareScene.js +++ b/radicale/web/internal_data/js/scenes/CreateEditShareScene.js @@ -35,10 +35,9 @@ export class CreateEditShareScene { * @param {string} password * @param {import("../models/collection.js").Collection} collection * @param {string} shareType - * @param {function():void} onclose * @param {Share} [share] If provided, the scene will be in edit mode. */ - constructor(user, password, collection, shareType, onclose, share) { + constructor(user, password, collection, shareType, share) { let edit = !!share; let pathMapped = collection.href; /** @type {HTMLElement} */ let html_scene = document.getElementById("newshare"); @@ -83,7 +82,6 @@ export class CreateEditShareScene { if (scene_index !== null) { pop_scene(scene_index - 1); } - if (onclose) onclose(); } catch (err) { console.error(err); } @@ -119,7 +117,6 @@ export class CreateEditShareScene { errorHandler.setError(error); } else { pop_scene(scene_index - 1); - if (onclose) onclose(); } }; diff --git a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js index f424c9bc..345f7445 100644 --- a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +++ b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js @@ -21,6 +21,7 @@ import { Share, reload_sharing_list, update_incoming_share } from "../api/sharing.js"; import { ErrorHandler } from "../utils/error.js"; +import { displayPermissions } from "../utils/permissions.js"; import { LoadingScene } from "./LoadingScene.js"; import { Scene, pop_scene, push_scene, scene_stack } from "./scene_manager.js"; @@ -99,7 +100,7 @@ export class IncomingSharingScene { let pathortoken_td = node.querySelector("[data-name=pathortoken]"); let owner_td = node.querySelector("[data-name=owner]"); - let permissions_td = node.querySelector("[data-name=permissions]"); + 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]")); @@ -110,7 +111,7 @@ export class IncomingSharingScene { pathortoken_td.textContent = displayPath; owner_td.textContent = share.Owner; - permissions_td.textContent = share.Permissions; + displayPermissions(share.Permissions, permissions_td); enabled_cb.checked = share.EnabledByUser; hidden_cb.checked = share.HiddenByUser; diff --git a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js index d17548cc..779e5aba 100644 --- a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js @@ -27,6 +27,7 @@ import { } from "../api/sharing.js"; import { Collection } from "../models/collection.js"; import { ErrorHandler } from "../utils/error.js"; +import { displayPermissions } from "../utils/permissions.js"; import { CreateEditShareScene } from "./CreateEditShareScene.js"; import { Scene, pop_scene, push_scene, scene_stack } from "./scene_manager.js"; @@ -73,16 +74,12 @@ export class ShareCollectionScene { } function onsharebytoken() { - let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "token", function () { - update_share_list(user, password, collection, errorHandler); - }); + let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "token"); push_scene(create_edit_share_scene, false); } function onsharebymap() { - let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "map", function () { - update_share_list(user, password, collection, errorHandler); - }); + let create_edit_share_scene = new CreateEditShareScene(user, password, collection, "map"); push_scene(create_edit_share_scene, false); } @@ -181,23 +178,11 @@ function add_share_row_node(user, password, collection, share, template, delete_ } let permissions = (share["Permissions"] || "").toLowerCase(); - if (permissions === "rw") { - node - .querySelector("[data-name=ro]") - .parentNode.removeChild(node.querySelector("[data-name=ro]")); - } else if (permissions === "r") { - node - .querySelector("[data-name=rw]") - .parentNode.removeChild(node.querySelector("[data-name=rw]")); - } else { - console.warn("Unknown permissions", permissions); - } + displayPermissions(permissions, node); /** @type {HTMLElement} */ let edit_btn = node.querySelector("[data-name=edit]"); edit_btn.onclick = function () { - let create_edit_share_scene = new CreateEditShareScene(user, password, collection, share.ShareType, function () { - update_share_list(user, password, collection, errorHandler); - }, share); + let create_edit_share_scene = new CreateEditShareScene(user, password, collection, share.ShareType, share); push_scene(create_edit_share_scene, false); }; diff --git a/radicale/web/internal_data/js/utils/permissions.js b/radicale/web/internal_data/js/utils/permissions.js new file mode 100644 index 00000000..02ca4c6f --- /dev/null +++ b/radicale/web/internal_data/js/utils/permissions.js @@ -0,0 +1,38 @@ +/** + * This file is part of Radicale Server - Calendar Server + * Copyright © 2026-2026 Max Berger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @param {string} permissions + * @param {HTMLElement} node + */ +export function displayPermissions(permissions, node) { + permissions = (permissions || "").toLowerCase(); + if (permissions === "rw") { + const roElement = node.querySelector("[data-name=ro]"); + if (roElement) { + roElement.parentNode.removeChild(roElement); + } + } else if (permissions === "r") { + const rwElement = node.querySelector("[data-name=rw]"); + if (rwElement) { + rwElement.parentNode.removeChild(rwElement); + } + } else { + console.warn("Unknown permissions", permissions); + } +}