From 2a24dc524c3352e660d99e08eafd302cb407365e Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sun, 15 Mar 2026 22:24:00 +0100 Subject: [PATCH 1/2] Unified data cache into single utility class --- radicale/web/internal_data/js/api/sharing.js | 22 +-- .../js/scenes/CollectionsScene.js | 48 ++---- .../js/scenes/CreateEditCollectionScene.js | 2 + .../js/scenes/DeleteCollectionScene.js | 2 + .../js/scenes/IncomingSharingScene.js | 41 +---- .../web/internal_data/js/scenes/LoginScene.js | 4 + .../js/scenes/ShareCollectionScene.js | 66 ++++---- .../js/scenes/UploadCollectionScene.js | 4 + .../js/utils/collections_cache.js | 154 ++++++++++++++++++ 9 files changed, 233 insertions(+), 110 deletions(-) create mode 100644 radicale/web/internal_data/js/utils/collections_cache.js diff --git a/radicale/web/internal_data/js/api/sharing.js b/radicale/web/internal_data/js/api/sharing.js index f9385b51..d145c3e1 100644 --- a/radicale/web/internal_data/js/api/sharing.js +++ b/radicale/web/internal_data/js/api/sharing.js @@ -34,9 +34,6 @@ import { CollectionType } from "../models/collection.js"; * @property {SharingFeatures} [sharing] */ -/** @type {ServerFeatures} */ -export let server_features = {}; - /** * @param {string} user * @param {string} password @@ -95,7 +92,7 @@ function call_sharing_api( /** * @param {string} user * @param {string} password - * @param {function():void} callback + * @param {function(import("../api/sharing.js").ServerFeatures, ?string):void} callback */ export function discover_server_features(user, password, callback) { call_sharing_api( @@ -104,16 +101,19 @@ export function discover_server_features(user, password, callback) { "all/info", {}, function (response) { - server_features["sharing"] = JSON.parse(response); - callback(); + try { + let features = { "sharing": JSON.parse(response) }; + callback(features, null); + } catch (e) { + callback({}, e.message); + } }, function () { // sharing is disabled on the server - server_features["sharing"] = {}; - callback(); + callback({ "sharing": {} }, null); }, function (error) { - console.error("Failed to discover sharing features: " + error); + callback({}, error); }, ); } @@ -147,7 +147,7 @@ export class Share { */ export function reload_sharing_list(user, password, collection, callback) { let body = collection ? { PathMapped: collection.href } : {}; - call_sharing_api( + return call_sharing_api( user, password, "all/list", @@ -160,7 +160,7 @@ export function reload_sharing_list(user, password, collection, callback) { null, // on_not_found function (error) { callback([], error); - } + }, ); } diff --git a/radicale/web/internal_data/js/scenes/CollectionsScene.js b/radicale/web/internal_data/js/scenes/CollectionsScene.js index 1c850acd..2ccb3b6d 100644 --- a/radicale/web/internal_data/js/scenes/CollectionsScene.js +++ b/radicale/web/internal_data/js/scenes/CollectionsScene.js @@ -19,17 +19,14 @@ * along with this program. If not, see . */ -import { get_collections } from "../api/api.js"; -import { discover_server_features } from "../api/sharing.js"; - import { SERVER } from "../constants.js"; import { Collection, CollectionType } from "../models/collection.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { bytesToHumanReadable } from "../utils/misc.js"; import { CreateEditCollectionScene } from "./CreateEditCollectionScene.js"; import { DeleteCollectionScene } from "./DeleteCollectionScene.js"; import { IncomingSharingScene } from "./IncomingSharingScene.js"; -import { LoadingScene } from "./LoadingScene.js"; -import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.js"; +import { Scene, push_scene } from "./scene_manager.js"; import { ShareCollectionScene, maybe_enable_sharing_options } from "./ShareCollectionScene.js"; import { UploadCollectionScene } from "./UploadCollectionScene.js"; @@ -51,8 +48,6 @@ export class CollectionsScene { /** @type {HTMLElement} */ let upload_btn = html_scene.querySelector("[data-name=upload]"); /** @type {HTMLElement} */ let incomingshares_btn = html_scene.querySelector("[data-name=incomingshares]"); - /** @type {?XMLHttpRequest} */ let collections_req = null; - /** @type {?Array} */ let child_collections = null; /** @type {Array} */ let nodes = []; function onnew() { @@ -132,6 +127,13 @@ export class CollectionsScene { let heightOfNavBar = navBar.offsetHeight + "px"; html_scene.style.marginTop = heightOfNavBar; html_scene.style.height = "calc(100vh - " + heightOfNavBar + ")"; + + // Clear old nodes + nodes.forEach(function (node) { + node.parentNode.removeChild(node); + }); + nodes = []; + collections.forEach(function (/** @type {Collection} */ collection) { /** @type {HTMLElement} */ let node = /** @type {HTMLElement} */(template.cloneNode(true)); node.classList.remove("hidden"); @@ -192,43 +194,20 @@ export class CollectionsScene { }); } - function update() { - let loading_scene = new LoadingScene(); - push_scene(loading_scene); - collections_req = get_collections(user, password, principal_collection, function (child_collections_, error) { - if (!is_current_scene(loading_scene)) { - return; - } - collections_req = null; - if (error) { - onerror(error); - pop_scene(); - } else { - child_collections = child_collections_; - pop_scene(); - } - }); - } this.show = function () { html_scene.classList.remove("hidden"); new_btn.onclick = onnew; upload_btn.onclick = onupload; incomingshares_btn.onclick = onincomingshares; - if (child_collections === null) { - update(); - discover_server_features(user, password, maybe_enable_sharing_options); - } else { - // from update loading scene - show_collections(child_collections); - } + collectionsCache.getChildCollections(user, password, principal_collection, onerror, show_collections); + collectionsCache.getServerFeatures(user, password, null, maybe_enable_sharing_options); }; this.hide = function () { html_scene.classList.add("hidden"); new_btn.onclick = null; upload_btn.onclick = null; incomingshares_btn.onclick = null; - child_collections = null; // remove collection nodes.forEach(function (node) { node.parentNode.removeChild(node); @@ -236,11 +215,6 @@ export class CollectionsScene { nodes = []; }; this.release = function () { - if (collections_req !== null) { - collections_req.abort(); - collections_req = null; - } - child_collections = null; }; } } \ No newline at end of file diff --git a/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js b/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js index 21a3cdf4..2d47a1b6 100644 --- a/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js @@ -22,6 +22,7 @@ import { create_collection, edit_collection } from "../api/api.js"; import { COLOR_RE } from "../constants.js"; import { Collection, CollectionType } from "../models/collection.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { ErrorHandler } from "../utils/error.js"; import { FormValidator, validate_color, validate_href } from "../utils/form_validator.js"; import { cleanHREFinput, onCleanHREFinput, random_hex, random_uuid } from "../utils/misc.js"; @@ -140,6 +141,7 @@ export class CreateEditCollectionScene { errorHandler.setError(error1); pop_scene(); } else { + collectionsCache.invalidate(); pop_to_parent(); } }; diff --git a/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js b/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js index 16546bbc..67e34ce6 100644 --- a/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/DeleteCollectionScene.js @@ -22,6 +22,7 @@ import { delete_collection } from "../api/api.js"; import { DELETE_CONFIRMATION_TEXT } from "../constants.js"; import { Collection } from "../models/collection.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { ErrorHandler } from "../utils/error.js"; import { FormValidator, validate_equals } from "../utils/form_validator.js"; import { LoadingScene } from "./LoadingScene.js"; @@ -72,6 +73,7 @@ export class DeleteCollectionScene { errorHandler.setError(error1); pop_scene(); } else { + collectionsCache.invalidate(); pop_to_parent(); } }); diff --git a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js index f1f30493..1cc1ac2c 100644 --- a/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +++ b/radicale/web/internal_data/js/scenes/IncomingSharingScene.js @@ -19,11 +19,11 @@ * along with this program. If not, see . */ -import { Share, reload_sharing_list, update_incoming_share } from "../api/sharing.js"; +import { update_incoming_share } from "../api/sharing.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { ErrorHandler } from "../utils/error.js"; import { displayPermissions } from "../utils/permissions.js"; -import { LoadingScene } from "./LoadingScene.js"; -import { Scene, is_current_scene, pop_scene, push_scene } from "./scene_manager.js"; +import { Scene, pop_scene } from "./scene_manager.js"; /** * @implements {Scene} @@ -45,14 +45,13 @@ export class IncomingSharingScene { let error_handler = new ErrorHandler(error_element); /** @type {Array} */ let nodes = []; - /** @type {?Array} */ let shares_cache = null; function on_cancel() { pop_scene(); } /** - * @param {Share} share + * @param {import("../api/sharing.js").Share} share * @param {HTMLElement} node */ function toggle_share(share, node) { @@ -79,12 +78,14 @@ export class IncomingSharingScene { enabled_cb.checked = old_enabled; shown_cb.checked = !old_hidden; shown_cb.disabled = !old_enabled; + } else { + collectionsCache.invalidate(); } }); } /** - * @param {Share[]} shares + * @param {import("../api/sharing.js").Share[]} shares */ function render_shares(shares) { // clear old nodes @@ -138,34 +139,11 @@ export class IncomingSharingScene { }); } - function update() { - let loading_scene = new LoadingScene(); - push_scene(loading_scene); - - error_handler.clearError(); - - reload_sharing_list(user, password, null, function (shares, error) { - if (!is_current_scene(loading_scene)) { - return; - } - if (error) { - error_handler.setError(error); - pop_scene(); - } else { - shares_cache = shares; - pop_scene(); - } - }); - } - this.show = function () { html_scene.classList.remove("hidden"); cancel_btn.onclick = on_cancel; - if (shares_cache === null) { - update(); - } else { - render_shares(shares_cache); - } + error_handler.clearError(); + collectionsCache.getIncomingShares(user, password, error_handler.setError, render_shares); }; this.hide = function () { @@ -182,7 +160,6 @@ export class IncomingSharingScene { } }); nodes = []; - shares_cache = null; }; } } diff --git a/radicale/web/internal_data/js/scenes/LoginScene.js b/radicale/web/internal_data/js/scenes/LoginScene.js index 45d45725..d671487d 100644 --- a/radicale/web/internal_data/js/scenes/LoginScene.js +++ b/radicale/web/internal_data/js/scenes/LoginScene.js @@ -20,6 +20,7 @@ */ import { get_principal } from "../api/api.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { ErrorHandler } from "../utils/error.js"; import { FormValidator, validate_non_empty } from "../utils/form_validator.js"; import { CollectionsScene } from "./CollectionsScene.js"; @@ -58,6 +59,7 @@ export class LoginScene { function onlogin() { try { + collectionsCache.invalidate(); read_form(); let password = password_form.value; if (!validator.validate()) { @@ -99,6 +101,7 @@ export class LoginScene { let onlogout = function () { try { + collectionsCache.invalidate(); user = ""; pop_to_root(); } catch (err) { @@ -118,6 +121,7 @@ export class LoginScene { function refresh() { // The easiest way to refresh is to push a LoadingScene onto the stack and then pop it // forcing the scene below it, the Collections Scene to refresh itself. + collectionsCache.invalidate(); push_scene(new LoadingScene()); pop_scene(); } diff --git a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js index 393a4634..5128c038 100644 --- a/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/ShareCollectionScene.js @@ -23,9 +23,9 @@ import { delete_share_by_map, delete_share_by_token, reload_sharing_list, - server_features, } from "../api/sharing.js"; import { Collection } from "../models/collection.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { ErrorHandler } from "../utils/error.js"; import { displayPermissions } from "../utils/permissions.js"; import { CreateEditShareScene } from "./CreateEditShareScene.js"; @@ -86,35 +86,38 @@ export class ShareCollectionScene { this.release(); html_scene.classList.remove("hidden"); cancel_btn.onclick = oncancel; - if (server_features.sharing && server_features.sharing.PermittedCreateCollectionByToken) { - if (share_by_token_btn) { - share_by_token_btn.classList.remove("hidden"); - share_by_token_btn.onclick = onsharebytoken; + + collectionsCache.getServerFeatures(user, password, errorHandler.setError, (features) => { + if (features.sharing && features.sharing.PermittedCreateCollectionByToken) { + if (share_by_token_btn) { + share_by_token_btn.classList.remove("hidden"); + share_by_token_btn.onclick = onsharebytoken; + } + } else { + if (share_by_token_btn) share_by_token_btn.classList.add("hidden"); } - } else { - if (share_by_token_btn) share_by_token_btn.classList.add("hidden"); - } - if (server_features.sharing && server_features.sharing.FeatureEnabledCollectionByToken) { - if (share_by_token_div) share_by_token_div.classList.remove("hidden"); - } else { - 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; + if (features.sharing && features.sharing.FeatureEnabledCollectionByToken) { + if (share_by_token_div) share_by_token_div.classList.remove("hidden"); + } else { + if (share_by_token_div) share_by_token_div.classList.add("hidden"); } - } 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 { - if (share_by_map_div) share_by_map_div.classList.add("hidden"); - } + if (features.sharing && 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 (features.sharing && features.sharing.FeatureEnabledCollectionByMap) { + if (share_by_map_div) share_by_map_div.classList.remove("hidden"); + } else { + if (share_by_map_div) share_by_map_div.classList.add("hidden"); + } + }); title.textContent = collection.displayname || collection.href; update_share_list(user, password, collection, errorHandler); @@ -231,10 +234,13 @@ function add_share_rows(user, password, collection, shares, errorHandler) { }); } -export function maybe_enable_sharing_options() { - if (!server_features.sharing) return; - let map_is_enabled = server_features.sharing.FeatureEnabledCollectionByMap || false; - let token_is_enabled = server_features.sharing.FeatureEnabledCollectionByToken || false; +/** + * @param {import('../api/sharing.js').ServerFeatures} features + */ +export function maybe_enable_sharing_options(features) { + if (!features || !features.sharing) return; + let map_is_enabled = features.sharing.FeatureEnabledCollectionByMap || false; + let token_is_enabled = features.sharing.FeatureEnabledCollectionByToken || false; if (map_is_enabled || token_is_enabled) { let share_options = document.querySelectorAll("[data-name=shareoption]"); for (let i = 0; i < share_options.length; i++) { diff --git a/radicale/web/internal_data/js/scenes/UploadCollectionScene.js b/radicale/web/internal_data/js/scenes/UploadCollectionScene.js index 310a7ef9..b012885f 100644 --- a/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +++ b/radicale/web/internal_data/js/scenes/UploadCollectionScene.js @@ -21,6 +21,7 @@ import { upload_collection } from "../api/api.js"; import { Collection } from "../models/collection.js"; +import { collectionsCache } from "../utils/collections_cache.js"; import { ErrorHandler } from "../utils/error.js"; import { FormValidator, validate_files, validate_href } from "../utils/form_validator.js"; import { cleanHREFinput, onCleanHREFinput, random_uuid } from "../utils/misc.js"; @@ -127,6 +128,9 @@ export class UploadCollectionScene { function onclose() { try { + if (results.length > 0) { + collectionsCache.invalidate(); + } pop_scene(); } catch (err) { console.error(err); diff --git a/radicale/web/internal_data/js/utils/collections_cache.js b/radicale/web/internal_data/js/utils/collections_cache.js new file mode 100644 index 00000000..7110df0f --- /dev/null +++ b/radicale/web/internal_data/js/utils/collections_cache.js @@ -0,0 +1,154 @@ +/** + * This file is part of Radicale Server - Calendar Server + * Copyright © 2017-2024 Unrud + * Copyright © 2023-2024 Matthew Hana + * Copyright © 2024-2025 Peter Bieringer + * 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 . + */ + +import { get_collections } from "../api/api.js"; +import { discover_server_features, reload_sharing_list } from "../api/sharing.js"; +import { LoadingScene } from "../scenes/LoadingScene.js"; +import { is_current_scene, pop_scene, push_scene } from "../scenes/scene_manager.js"; + +class CollectionsCache { + constructor() { + this.child_collections = null; + /** @type {?Array} */ this.incoming_shares = null; + this.server_features = null; + /** @type {?XMLHttpRequest} */ this.collections_req = null; + /** @type {?XMLHttpRequest} */ this.shares_req = null; + } + + invalidate() { + this.child_collections = null; + this.incoming_shares = null; + this.server_features = null; + if (this.collections_req) { + this.collections_req.abort(); + this.collections_req = null; + } + if (this.shares_req) { + this.shares_req.abort(); + this.shares_req = null; + } + } + + /** + * @param {string} user + * @param {string} password + * @param {import("../models/collection.js").Collection} principal_collection + * @param {function(string):void} onerror + * @param {function(Array, Array):void} displayData + */ + getChildCollections(user, password, principal_collection, onerror, displayData) { + if (this.child_collections !== null && this.incoming_shares !== null) { + displayData(this.child_collections, this.incoming_shares); + return; + } + + let loading_scene = new LoadingScene(); + push_scene(loading_scene); + + let collections = null; + let shares = null; + let error = null; + + let check_if_completed = () => { + if (!is_current_scene(loading_scene)) { + return; + } + if (error) { + onerror(error); + pop_scene(); + } else if (collections !== null && shares !== null) { + this.child_collections = collections; + this.incoming_shares = shares; + displayData(this.child_collections, this.incoming_shares); + pop_scene(); + } + }; + + this.collections_req = get_collections(user, password, principal_collection, (c, e) => { + this.collections_req = null; + if (e) error = e; + collections = c || []; + check_if_completed(); + }); + + this.shares_req = reload_sharing_list(user, password, null, (s, e) => { + this.shares_req = null; + if (e) error = e; + shares = s || []; + check_if_completed(); + }); + } + + /** + * @param {string} user + * @param {string} password + * @param {function(string):void} onerror + * @param {function(Array):void} displayData + */ + getIncomingShares(user, password, onerror, displayData) { + if (this.incoming_shares !== null) { + displayData(this.incoming_shares); + return; + } + + let loading_scene = new LoadingScene(); + push_scene(loading_scene); + + this.shares_req = reload_sharing_list(user, password, null, (shares, error) => { + if (!is_current_scene(loading_scene)) { + return; + } + this.shares_req = null; + if (error) { + onerror(error); + pop_scene(); + } else { + this.incoming_shares = shares; + displayData(this.incoming_shares); + pop_scene(); + } + }); + } + + /** + * @param {string} user + * @param {string} password + * @param {function(string):void} onerror + * @param {function(import("../api/sharing.js").ServerFeatures):void} displayData + */ + getServerFeatures(user, password, onerror, displayData) { + if (this.server_features !== null) { + displayData(this.server_features); + return; + } + + discover_server_features(user, password, (features, error) => { + if (error) { + if (onerror) onerror(error); + } else { + this.server_features = features; + displayData(this.server_features); + } + }); + } +} + +export const collectionsCache = new CollectionsCache(); From f6c5a3753afc8fa10827761e2c78376d2c28a299 Mon Sep 17 00:00:00 2001 From: Max Berger Date: Sun, 15 Mar 2026 23:21:17 +0100 Subject: [PATCH 2/2] Display incoming share owner and hide some action buttons --- integ_tests/test_sharing.py | 30 ++++++++++++++++--- radicale/web/internal_data/index.html | 1 + .../js/scenes/CollectionsScene.js | 20 ++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/integ_tests/test_sharing.py b/integ_tests/test_sharing.py index ecc667c9..6138514f 100644 --- a/integ_tests/test_sharing.py +++ b/integ_tests/test_sharing.py @@ -297,7 +297,8 @@ def test_share_by_map_validation(page: Page, radicale_server: str) -> None: ).to_have_count(1) -def test_incoming_shares(page: Page, radicale_server: str) -> None: +@pytest.mark.parametrize("permissions", ["ro", "rw"]) +def test_incoming_shares(page: Page, radicale_server: str, permissions: str) -> None: # 1. Admin logs in and creates a map share for 'max' login(page, radicale_server) create_collection(page, radicale_server) @@ -307,6 +308,8 @@ def test_incoming_shares(page: Page, radicale_server: str) -> None: page.click('button[data-name="sharebymap"]') page.locator('input[data-name="shareuser"]').fill("max") page.locator('input[data-name="sharehref"]').fill("mapped") + if permissions == "rw": + page.check("#newshare_attr_permissions_rw") page.click('#newshare button[data-name="submit"]') expect( page.locator("tr[data-name='sharemaprowtemplate']:not(.hidden)") @@ -372,12 +375,31 @@ def test_incoming_shares(page: Page, radicale_server: str) -> None: ) ).to_be_checked() - # 6. Assert no error was shown - expect(page.locator('#incomingsharingscene span[data-name="error"]')).to_be_hidden() - + # 6. Verify "shared by admin" and button visibility in the collection article page.click('#incomingsharingscene button[data-name="cancel"]') expect(page.locator("#incomingsharingscene")).to_be_hidden() + article = page.locator("article:not(.hidden)").first + expect(article.locator('[data-name="shared-by"]')).to_be_visible() + expect(article.locator('[data-name="shared-by-owner"]')).to_have_text("admin") + + # Action buttons are only visible on mouseover + article.hover() + + # Share and delete buttons should be hidden for all incoming shares + expect(article.locator('a[data-name="share"]')).to_be_hidden() + expect(article.locator('[data-name="shareoption"]')).to_be_hidden() + expect(article.locator('a[data-name="delete"]')).to_be_hidden() + + # Edit button depends on permissions + if permissions == "rw": + expect(article.locator('a[data-name="edit"]')).to_be_visible() + else: + expect(article.locator('a[data-name="edit"]')).to_be_hidden() + + # 7. Assert no error was shown + expect(page.locator('#incomingsharingscene span[data-name="error"]')).to_be_hidden() + def test_no_incoming_shares_message(page: Page, radicale_server: str) -> None: # 1. Max logs in diff --git a/radicale/web/internal_data/index.html b/radicale/web/internal_data/index.html index 02d32749..a21866b0 100644 --- a/radicale/web/internal_data/index.html +++ b/radicale/web/internal_data/index.html @@ -74,6 +74,7 @@

Title

+ Address book Calendar, journal and tasks Calendar and journal diff --git a/radicale/web/internal_data/js/scenes/CollectionsScene.js b/radicale/web/internal_data/js/scenes/CollectionsScene.js index 2ccb3b6d..a8467449 100644 --- a/radicale/web/internal_data/js/scenes/CollectionsScene.js +++ b/radicale/web/internal_data/js/scenes/CollectionsScene.js @@ -121,8 +121,9 @@ export class CollectionsScene { /** * @param {any[]} collections + * @param {import("../api/sharing.js").Share[]} shares */ - function show_collections(collections) { + function show_collections(collections, shares) { /** @type {HTMLElement} */ let navBar = document.querySelector("#logoutview"); let heightOfNavBar = navBar.offsetHeight + "px"; html_scene.style.marginTop = heightOfNavBar; @@ -164,6 +165,23 @@ export class CollectionsScene { node.querySelector("[data-name=" + e + "]").classList.add("hidden"); } }); + let share_info = node.querySelector("[data-name=shared-by]"); + let share = (shares || []).find(s => s.ShareType === "map" && (s.PathOrToken || "").replace(/\/+$/, "") === (collection.href || "").replace(/\/+$/, "")); + if (share && share.Owner !== user) { + share_info.classList.remove("hidden"); + node.querySelector("[data-name=shared-by-owner]").textContent = share.Owner; + let share_option = node.querySelector("[data-name=shareoption]"); + if (share_option) { + share_option.classList.add("hidden"); + share_option.removeAttribute("data-name"); + } + delete_btn.classList.add("hidden"); + if (!/w/i.test(share.Permissions || "")) { + edit_btn.classList.add("hidden"); + } else { + edit_btn.classList.remove("hidden"); + } + } title_form.textContent = collection.displayname || collection.href; if (title_form.textContent.length > 30) { title_form.classList.add("smalltext");